Skip to content

Instantly share code, notes, and snippets.

@Aboudjem
Last active May 31, 2026 00:19
Show Gist options
  • Select an option

  • Save Aboudjem/fad46568b34a09bf453fd48a57123252 to your computer and use it in GitHub Desktop.

Select an option

Save Aboudjem/fad46568b34a09bf453fd48a57123252 to your computer and use it in GitHub Desktop.
Adam's M5 Max install — power user, ADHD, bounty-lab, AI/MLX, fanout-verified May 2026

M5 Max — Setup Status

Auto-generated by ~/m5-setup/verify.sh --status-md Last run: 2026-05-30 17:55 CDT · PASS=124 / FAIL=1 / WARN=2 Legend: ✅ done · 🟡 partial · ⬜ todo · ⚠️ drift

Command Center — prompts run

# Prompt Run? Last run Artifact
0 Status 2026-05-30 17:55 00-STATUS.md (this file)
1 Audit
2 Install
3 Ghostty Polish 2026-05-30 17:04 "v2 polish" marker in ghostty config
4 Claude Code Config 2026-05-30 16:43 11 MCPs registered
5 Verification
6 Debugging n/a
7 Cleanup + Docs (no on-disk marker — manually tick when done)
8 Final Review
Phase Status Note
1. Foundation (Xcode, brew, arch) arm64, macOS 26.5
2. Brew formulae (44 required) 🟡 run brew bundle install --file=~/m5-setup/Brewfile if missing
3. Brew casks (32 required) 🟡 font-jetbrains-mono-nerd-font required by Ghostty
4. Dev runtimes (mise: node/python/rust/go) go 1.26.3
5. Python tools (slither/halmos/eth-wake via uv)
6. Foundry (forge/cast/anvil) curl -L https://foundry.paradigm.xyz | bash && foundryup
7. npm globals (claude/vercel/pnpm) 2.1.156 (Claude Code)
8. Ollama daemon + models 4 models installed
9. Configs (.zshrc, ghostty, starship, atuin, mise, claude)
10. Claude Code env in ~/.zshrc
11. ANTHROPIC_BASE_URL guardrail claude stays on Anthropic cloud
12. macOS defaults (Finder/KeyRepeat/Dock/dashes)
13. GPU cap (live + LaunchDaemon) 122880 MB
14. GPU cap persistence (LaunchDaemon) reverts on reboot if missing
15. SSH + GitHub auth gh auth login + gh ssh-key add
16. Git commit signing (SSH via 1Password)
17. MLX project (~/1_PROJECTS/mlx-lab)
18. Cleanup (no dropped formulae/casks) dropped: procs, bottom, httpie, mstystudio, msty, rectangle, sleeve, wezterm, spotify

Raw counts

  • ✅ PASS = 124
  • ⬜ FAIL = 1
  • ⚠️ WARN = 2

Next prompts to run

  • Install phase looks clean
  • Prompt 3 (Ghostty Polish) — apply v2 polish
  • (env guardrail green — claude stays on cloud)
  • Prompt 7 (Cleanup + Docs) — 2 warnings to triage

Refresh this file: ~/m5-setup/verify.sh --status-md Push to gist: gh gist edit fad46568b34a09bf453fd48a57123252 -f 00-STATUS.md ~/m5-setup/00-STATUS.md

Adam — M5 Max install + config, fully explained (2026-05-27)

Tip

TL;DR — if you just want it done: clone this gist and run :

# HTTPS clone — works on a fresh Mac. `gh gist clone` uses SSH and fails until
# gist.github.com host-key is trusted, which it isn't on a clean install.
git clone https://gist.github.com/fad46568b34a09bf453fd48a57123252.git ~/m5-setup
cd ~/m5-setup
chmod +x setup.sh verify.sh
brew bundle install --file=Brewfile
./setup.sh

setup.sh does EVERYTHING in this doc automatically — writes all configs, generates SSH key, uploads to GitHub via gh, pulls Ollama models, sets sysctl, etc. The walkthrough below explains what each step does in case you want to understand or do it piece-by-piece.

Note

Step-by-step setup where every command is explained. Not "paste this", but "here's what this does to your Mac and why". For when you want to understand, not just copy.


Note

Where we are — 2026-05-27 18:18 · PASS 108 / FAIL 18 / WARN 9

1 Audit 2 Install 3 Ghostty 4 Claude 5 Verify 7 Cleanup 8 Review

Next: Prompt 2 (Install) — 18 items missing from the spec. Full detail: 00-STATUS.md · regen with ./verify.sh --status-md. Edit this block by hand as you go, or let Prompts 1/2/5/7 refresh 00-STATUS.md for you.


🎛️ Command Center — 8 paste-ready prompts

This section is the operational interface for the M5 install. The walkthrough below is the reference doc; these 8 prompts are what you actually paste into Claude Code on the new machine.

How to use: Open Claude Code in any fresh session on the M5. Read the conversational note above the prompt you want, then copy the entire code block (the lines between the triple backticks) and paste it. Each block carries its own Hard Context, so a fresh session can run it cold with no prior conversation history.

The eight prompts cover one full setup cycle, in order:

# Prompt What it does When to run
0 Status Read 00-STATUS.md (the 5th gist file) to see ✅ done / 🟡 partial / ⬜ todo / ⚠️ drift for every phase. Regenerable via ~/m5-setup/verify.sh --status-md Any time — start here on every session
1 Audit Reads current state of the Mac, writes a 3-bucket report First thing on the new machine
2 Install Installs everything from the MISSING list After Prompt 1
3 Ghostty Polish Verifies the 10-item polish block is present in Ghostty config (appends if missing) After Prompt 2 (config must exist)
4 Claude Code Config Configures Claude Code itself: settings, env, base MCPs After Prompt 2
5 Verification Smoke-tests the critical paths end-to-end After Prompts 2-4
6 Debugging When something breaks, investigate scientifically Any time after install
7 Cleanup + Docs Removes dropped items, syncs docs with reality Any time after install
8 Final Review Adversarial pass over the gist itself Before declaring done

Tip

The quickest "where am I" check: open 00-STATUS.md in the gist (or in ~/m5-setup/00-STATUS.md locally). It shows what's done, what's pending, and which prompt to run next. Regenerated by running ~/m5-setup/verify.sh --status-md — every Prompt 1, 2, 5, or 7 run refreshes it.

This is scoped to installation, setup, and configuration of a clean M5 only. No project repositories are cloned yet, no dotfiles are imported from the old Mac, no project-specific plugins are installed. That comes in a separate cycle once the base is solid.

Important

Paste the whole code block, including the HARD CONTEXT lines at the top. The conversational text and tables above each block are for you, the human — don't paste those.


Prompt 1 — Audit (read-only)

The first thing you run on the new Mac. This prompt is purely diagnostic — Claude reads the current state, runs the gist's verify.sh against this machine, and writes a clean three-bucket report you can review before any installs happen. Nothing gets changed; nothing gets installed. On a freshly-unboxed M5 you should expect a long MISSING list (most of the spec) and a short WORKING list (Xcode CLI, Homebrew if you pre-installed it, defaults).

When to use: First action on the new Mac, or any time you want a "where am I" snapshot. Expected outcome: A markdown report at /tmp/m5-audit-YYYYMMDD.md showing WORKING / MISSING / CLEANUP. Prompt 2 reads this file.

/goal — Audit my M5 Max against the gist spec. READ-ONLY.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup
  Clone if missing (HTTPS — `gh gist clone` fails on fresh Mac because
  the gist.github.com SSH host-key isn't trusted yet):
    git clone https://gist.github.com/fad46568b34a09bf453fd48a57123252.git ~/m5-setup
- Files in gist: adam-mac-setup.md (this walkthrough), Brewfile, setup.sh, verify.sh, 00-STATUS.md

LOCKED DECISIONS (do NOT re-debate):
- Clean install · no Migration Assistant · no project import
- Ghostty + Antidote + Starship + Atuin + mise + uv
- Brave primary / Chrome backup / Firefox testing
- VS Code (chosen over Cursor for unrestricted local-LLM use via Continue.dev)
- Obsidian for ALL notes (no Notion)
- Apple Music (no Spotify)
- OrbStack (no Docker Desktop)
- Ollama at localhost:11434 = LOCAL MODELS ONLY. `claude` (Anthropic CLI, Max sub)
  stays on cloud — `ANTHROPIC_BASE_URL` MUST remain unset. Local-model coding uses
  a SEPARATE tool (opencode primary, Crush fallback) wired to Ollama via its
  own config, NEVER by repointing `claude`.
- whisper-cpp ONLY (NEVER `pip install openai-whisper`)
- Foundry via curl installer (foundryup → ~/.foundry/bin), NOT brew — matches M5 reality
- sui + supabase now in homebrew-core (no taps)
- iogpu.wired_limit_mb=122880 (120 GB cap — NEVER 124000)
- Dropped (do not re-add): mstystudio, msty, rectangle, sleeve, wezterm, procs, bottom, httpie, spotify

RULES:
- Mnemosyne: every "latest"/"best" claim must trace to a primary URL
  (formulae.brew.sh, github releases, vendor changelog). Tag [UNVERIFIED] otherwise.
- Git identity: commit as Adam Boudjemaa <boudjemaa.adam@gmail.com>.
  NEVER use `-c user.name=...` overrides on `git commit`.
- This is a FRESH machine. No project repos cloned yet. No dotfile imports.
  Pure install + setup + config.

DO:
1. Clone the gist to ~/m5-setup if it isn't already there:
     git clone https://gist.github.com/fad46568b34a09bf453fd48a57123252.git ~/m5-setup
2. chmod +x ~/m5-setup/verify.sh ~/m5-setup/setup.sh
3. Run ~/m5-setup/verify.sh > /tmp/verify-$(date +%Y%m%d-%H%M).log 2>&1
4. Independent checks (run in parallel where possible):
   - claude --version + which claude
   - gh auth status
   - mise current
   - ollama list
   - forge/cast/anvil --version
   - git config --global --get user.signingkey + gpg.format
   - sw_vers, df -h /, uname -m
5. Write /tmp/m5-audit-$(date +%Y%m%d).md with three buckets:
   ✅ WORKING (every PASS line), ❌ MISSING (every FAIL + the exact TO_INSTALL command),
   ⚠️ CLEANUP (every WARN + the TO_REMOVE command + the macOS defaults that need applying).
   Rank items in MISSING by load-bearing-ness: (1) blocks other tools (e.g. mise blocks node
   blocks Claude Code), (2) blocks daily workflow (Ghostty, Claude Code),
   (3) one-off polish (CleanShot, Shottr, Stats, etc).

DO NOT:
- Install anything
- Edit any config file
- Run any TO_REMOVE / uninstall commands
- Re-debate any locked decision

SUCCESS:
- /tmp/m5-audit-YYYYMMDD.md exists with all three sections populated
- Audit names file:line for every FAIL so the next session can act precisely
- One-paragraph end-of-turn summary: PASS/FAIL/WARN counts + the top 5 most
  load-bearing missing items

Prompt 2 — Install (idempotent)

The heavy lifter. After the audit identifies what's missing, this prompt fills the gap. It walks through every MISSING item from the audit, runs the exact install command, and logs the result. Idempotent — re-running it after a partial install (you closed the lid, you switched networks) safely picks up where it left off. Expect ~30 minutes of wall-clock on a fast connection and roughly 10 GB of downloads. Some items will surface as manual blockers (Hazel license, VS Code Settings Sync OAuth, 1Password OAuth) — those stop and ask, they don't fake completion.

When to use: Right after Prompt 1, with /tmp/m5-audit-*.md on disk. Expected outcome: verify.sh re-run shows ≤5 FAILs (license-gated and OAuth-blocked items only). Install log at /tmp/m5-install-fix.log.

/goal — Continue M5 setup from MISSING list in /tmp/m5-audit-*.md. Idempotent.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup (clone the Gist URL above + `.git` if missing)
- Files: adam-mac-setup.md, Brewfile, setup.sh, verify.sh, 00-STATUS.md

LOCKED DECISIONS (do NOT re-debate):
- Clean install · no Migration Assistant · no project import
- Ghostty + Antidote + Starship + Atuin + mise + uv
- Brave primary / Chrome backup / Firefox testing
- VS Code (chosen over Cursor for unrestricted local-LLM use via Continue.dev)
- Obsidian for ALL notes (no Notion)
- Apple Music (no Spotify)
- OrbStack (no Docker Desktop)
- Ollama at localhost:11434 = LOCAL MODELS ONLY (drives opencode/Crush/Continue.dev,
  NEVER the `claude` CLI — that stays on Anthropic cloud)
- ANTHROPIC_BASE_URL must stay UNSET — never export, never add to ~/.zshrc
- whisper-cpp ONLY (NEVER `pip install openai-whisper`)
- Foundry via curl installer (foundryup → ~/.foundry/bin), NOT brew
- sui + supabase in homebrew-core now (no taps)
- iogpu.wired_limit_mb=122880 (120 GB cap — NEVER 124000)
- Dropped: mstystudio, msty, rectangle, sleeve, wezterm, procs, bottom, httpie, spotify, notion, notion-calendar

RULES:
- Mnemosyne: tag [UNVERIFIED] anything not traceable to a primary source
- Git identity: Adam Boudjemaa <boudjemaa.adam@gmail.com>, no -c overrides
- FRESH machine: pure install/setup/config, no project imports

DO:
1. Read /tmp/m5-audit-YYYYMMDD.md (or re-run ~/m5-setup/verify.sh if missing)
2. Each MISSING formula/cask: `brew install <name>` — skip if `brew list <name>` succeeds
3. For tap-namespaced casks: `brew tap` first, THEN install:
   - cyfrin/tap → aderyn · stripe/stripe-cli → stripe
   - DO NOT re-add mystenlabs/tap or supabase/tap (moved to homebrew-core)
4. Python tools: `uv tool install slither-analyzer halmos eth-wake`
5. Activate mise THIS shell first (mandatory before npm; node lives in mise):
     eval "$(mise activate zsh)"
   Then npm globals (vercel+pnpm required by verify.sh):
     npm install -g @anthropic-ai/claude-code vercel pnpm
   Skip packages already in `npm list -g --depth=0`.
6. Ollama: `ollama pull huihui_ai/qwen3-abliterated:30b huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0 nomic-embed-text` —
   skip pulls already in `ollama list`
7. Foundry: if `forge` is missing, `curl -L https://foundry.paradigm.xyz | bash && foundryup`
8. macOS defaults from setup.sh §9 (Finder hidden, KeyRepeat=2, dock autohide, smart dashes OFF)
9. iogpu cap (asks sudo — surface to user, no silent-fail):
   sudo sysctl iogpu.wired_limit_mb=122880
   Persist via LaunchDaemon (Tahoe skips /etc/sysctl.conf). setup.sh §13 writes
   /Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist and loads it.
10. For each config missing from audit, re-run the relevant setup.sh block
    (uses `backup_if_exists`, preserves existing configs)
11. Log every install command + exit code + timestamp to /tmp/m5-install-fix.log

DO NOT:
- `brew uninstall` ANYTHING (cleanup is Prompt 7)
- Overwrite ~/.zshrc / ~/.gitconfig / ~/.config/* without a .bak first
- Use `sudo brew install` (brew refuses; investigate if seen)
- Re-pull Ollama models already present
- Skip `xcode-select -p` foundation check before anything
- Install spotify (Apple Music is locked)
- Export ANTHROPIC_BASE_URL anywhere (keep `claude` on Anthropic cloud)
- Continue past a manual blocker (Hazel, VS Code OAuth, 1Password OAuth);
  STOP and surface to user

SUCCESS:
- Re-run ~/m5-setup/verify.sh exits 0 OR shows only license-gated items
- /tmp/m5-install-fix.log has every command + exit code + timestamp
- One-paragraph end-of-turn: N installed, M skipped, K blockers surfaced
  (with specific action human needs)

Prompt 3 — Ghostty Polish (append only)

A safety-net top-up for the Ghostty config. The current setup.sh already bakes the v2 polish into the GHOSTTY heredoc (Section 21.3), so on a fresh M5 install this prompt is essentially a no-op. It exists for two cases: (a) the Ghostty config was written by an older setup.sh that pre-dates 2026-05-27 and needs the 10 items appended, or (b) someone hand-edited the config and the marker block went missing. The prompt verifies first, repairs only if needed — never overwrites.

When to use: After Prompt 2 if ~/.config/ghostty/config exists but lacks the v2 polish marker. Skipped automatically on fresh installs. Expected outcome: No-op on a fresh M5 (polish already present), or 10 new lines appended on an older config. Takes <30 seconds either way.

/goal — Append Ghostty v2 polish (10 items). APPEND ONLY — never overwrite.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup
- Files: adam-mac-setup.md, Brewfile, setup.sh, verify.sh, 00-STATUS.md
- This prompt operates on ~/.config/ghostty/config (written by setup.sh §7)

LOCKED DECISIONS:
- Theme = cyberdream (paint Neon Noir cyan/magenta/violet OVER it)
- Liquid Glass: background-opacity 0.85, background-blur macos-glass-regular
- Font = JetBrainsMono Nerd Font, size 14, thicken on
- The 10-item polish set is locked (Scout A, 2026-05-27, sourced to ghostty.org/docs)
- Append only — heredoc `>>` is the only allowed redirect, never `>`

RULES:
- Mnemosyne: every option must trace to ghostty.org/docs/config/reference
  or the 1.3.0 release notes. Tag [UNVERIFIED] otherwise.
- Git identity: Adam Boudjemaa <boudjemaa.adam@gmail.com>, no -c overrides
- FRESH machine: no project context required for this prompt

DO:
1. If ~/.config/ghostty/config does NOT exist, STOP — surface to user that
   they need to run Prompt 2 (Install) first. Do not create the file from scratch.
2. Back up: cp ~/.config/ghostty/config ~/.config/ghostty/config.bak-$(date +%s)
3. STOP-check: if `grep -q "v2 polish" ~/.config/ghostty/config` succeeds,
   the polish is already applied — exit cleanly without re-appending
4. Append the 10-item polish block via:
     cat >> ~/.config/ghostty/config <<'POLISH'
     # === v2 polish (Scout A, 2026-05-27) — 10 verified items, all sourced to ghostty.org/docs ===
     keybind = global:cmd+grave_accent=toggle_quick_terminal
     quick-terminal-position = top
     quick-terminal-animation-duration = 0.18
     quick-terminal-screen = main
     quick-terminal-autohide = true
     keybind = cmd+shift+p=toggle_command_palette
     title-command = printf '%s  %s' "$(basename "$PWD")" "$(git branch --show-current 2>/dev/null)"
     bell-features = visual,audio,no-system
     bell-audio-volume = 0.4
     notify-on-command-finish = unfocused
     notify-on-command-finish-action = notify
     notify-on-command-finish-after = 10s
     window-inherit-working-directory = true
     tab-inherit-working-directory = true
     split-inherit-working-directory = true
     scrollbar = system
     macos-option-as-alt = left
     unfocused-split-fill = #0A0A14
     resize-overlay = after-first
     window-padding-color = extend
     alpha-blending = native
     POLISH
5. Verify the bottom of the file: `tail -25 ~/.config/ghostty/config`
6. Reload Ghostty: `cmd+shift+,` (reload-config keybind) or quit + relaunch

DO NOT:
- Overwrite ~/.config/ghostty/config (heredoc `>` is forbidden — only `>>`)
- Rewrite the theme (cyberdream stays)
- Touch ~/.config/ghostty/themes/neon-noir
- Add options outside the 10-item set (every option must trace to a primary doc)
- Re-bind cmd+t / cmd+d / cmd+w / cmd+enter (already in base config)

SUCCESS:
- `grep -c "v2 polish" ~/.config/ghostty/config` returns ≥1
- Backup file ~/.config/ghostty/config.bak-NNNNNN exists
- Quick Terminal opens with cmd+` from any app in macOS
- Command Palette opens with cmd+shift+p inside Ghostty
- Tab title shows `<dirname>  <branch>` (not literal `$(basename...)`).
  If the title shows the literal command string, confirm
  `shell-integration-features = title` is set higher up in the config (it should be)

Prompt 4 — Claude Code Config (base only)

The Claude Code essentials, nothing project-specific. This prompt is for the brand-new M5: Claude Code is installed by Prompt 2, but it still needs its env vars wired into ~/.zshrc, its settings file pinned to the right model, the Ghostty multi-line paste workaround alias (cc), and a small set of generic MCPs that are useful in any project (filesystem, GitHub, Brave Search, Context7). No project-specific plugins yet — those go in a separate cycle once you've actually cloned project repos. Think of this as "Claude Code is now usable on this Mac with zero project context."

When to use: After Prompt 2 has installed @anthropic-ai/claude-code, before you clone any project repos. Expected outcome: claude starts cleanly, claude mcp list shows 4-5 generic MCPs, the cc shell alias works in Ghostty.

/goal — Configure Claude Code base on a fresh M5. Generic MCPs only — no project plugins.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup

LOCKED DECISIONS:
- Claude Code via npm globals: @anthropic-ai/claude-code
- Settings file: ~/.claude/settings.json (model pin + hooks + permissions)
- Shell alias: `cc='TERM=xterm-256color claude'` (Ghostty multi-line paste fix)
- Env vars in ~/.zshrc: CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1,
  CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=15 (matches setup.sh + settings.json)
- FRESH machine — GENERIC MCPs only. Project-specific plugins out of scope.

RULES:
- Mnemosyne: every MCP needs a verifiable upstream URL. Tag [UNVERIFIED] otherwise.
- Git identity: Adam Boudjemaa <boudjemaa.adam@gmail.com>, no -c overrides
- No project-specific plugins (project repos don't exist on a fresh M5).

DO:
1. Sanity: `claude --version` returns 2.x. If missing, surface and stop.
2. Confirm gh auth: `gh auth status`. If not, surface — user runs `gh auth login`.
3. Write ~/.claude/settings.json from setup.sh §7 template:
   - model pinned to current Opus suffix (read setup.sh verbatim — names rotate)
   - permissions: Bash, Read, Edit, Write, Grep, Glob allowed
   - hooks block if template has one
   Backup first: cp settings.json settings.json.bak-$(date +%s)
4. Append to ~/.zshrc (skip if marker already present):
     export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
     export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=15
     alias cc='TERM=xterm-256color claude'    # Ghostty multi-line paste workaround
5. Install GENERIC base MCPs only.
   Always-on (no key):
     claude mcp add --transport stdio --scope user filesystem -- npx -y @modelcontextprotocol/server-filesystem ~
     claude mcp add --transport http --scope user context7 https://mcp.context7.com/mcp
     claude mcp add --transport stdio --scope user sequential-thinking -- npx -y @modelcontextprotocol/server-sequential-thinking
   Key-gated (skip with warning if env var absent):
     [ -n "$GITHUB_PERSONAL_ACCESS_TOKEN" ] && claude mcp add --transport stdio --scope user github -- npx -y @modelcontextprotocol/server-github \
       || echo "skip github MCP — set GITHUB_PERSONAL_ACCESS_TOKEN first"
     [ -n "$BRAVE_API_KEY" ] && claude mcp add --transport stdio --scope user brave-search -- npx -y @modelcontextprotocol/server-brave-search \
       || echo "skip brave-search MCP — set BRAVE_API_KEY first"
6. Verify `claude mcp list` — expect 4-5 entries (filesystem, github, context7, sequential-thinking, optionally brave-search).
7. Smoke test in fresh Ghostty tab: `cc` launches Claude Code with TERM=xterm-256color (paste multi-line block — newlines must survive).

DO NOT:
- Set ANTHROPIC_BASE_URL or ANTHROPIC_AUTH_TOKEN — even commented out. `claude` MUST stay on Anthropic cloud (Claude Max). Local-model CLI is separate (§22.5).
- Install any project-specific plugin (no project repos on a fresh M5).
- Re-install Claude Code (Prompt 2 handled it)
- Skip the `claude --version` sanity check
- Overwrite ~/.claude/settings.json without backing it up first
- Guess a model name suffix — read setup.sh verbatim
- Add MCPs requiring API keys the user hasn't exported

SUCCESS:
- `claude --version` shows 2.x
- `claude mcp list` shows ≥4 generic MCPs
- `cc` alias works in Ghostty (multi-line paste preserves newlines)
- ~/.zshrc has the 2 CLAUDE_CODE_* env vars + `cc` alias
- ~/.claude/settings.json exists and parses as valid JSON (`jq . settings.json`)
- One-paragraph end-of-turn: N MCPs added, M skipped (no API key), settings written, alias active — Claude Code base-ready, project plugins deferred

Prompt 5 — Verification (smoke test)

Trust but verify. verify.sh from the gist checks installation completeness (is X on PATH?), but it doesn't check function (does X actually work end-to-end?). This prompt runs both: the full verify.sh plus 8 functional smoke tests covering the load-bearing tools — Foundry, Ollama, whisper-cpp, SSH→GitHub, Claude Code, mise runtimes, the GPU memory cap, and MLX Metal. Read-only, surfaces blockers, doesn't try to fix them.

When to use: After Prompts 2-4. Re-run any time you suspect drift. Expected outcome: A green report at /tmp/m5-verify-YYYYMMDD.md or a precise list of what's still broken and how to fix it.

/goal — Verify M5 setup end-to-end. Read-only smoke test.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup

LOCKED DECISIONS (do NOT re-debate):
- iogpu.wired_limit_mb must equal 122880 (120 GB cap on 128 GB Mac)
- whisper-cpp ONLY (the pip whisper path is broken on Apple Silicon)
- Foundry: forge/cast/anvil all v1.7+
- Ollama at localhost:11434, models huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0 + nomic-embed-text minimum.
  Verify `echo "$ANTHROPIC_BASE_URL"` returns EMPTY (else claude is hijacked to local).

RULES:
- Read-only — verification surfaces failures, does NOT fix them
- Mnemosyne: tag [UNVERIFIED] anything you can't confirm directly
- FRESH machine: tests must not assume any project repo exists

DO (run in parallel where possible):
1. Re-run ~/m5-setup/verify.sh > /tmp/verify-final-$(date +%Y%m%d-%H%M).log 2>&1
2. Smoke tests (8 functional checks):
   a. forge --version  &&  cast --version  &&  anvil --version  → all v1.7+
   b. ollama list  → confirms service running + lists installed models
   c. Whisper functional test:
        say "this is a test of the whisper system" -o /tmp/test.aiff
        ffmpeg -y -i /tmp/test.aiff -ar 16000 -ac 1 -c:a pcm_s16le /tmp/test.wav
        whisper-cli -m ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin -f /tmp/test.wav -l en
      → expect transcript with "test" in it within ~5 seconds
   d. ssh -T git@github.com 2>&1 | grep "successfully authenticated"
   e. claude --version  → returns 2.x cleanly
   f. mise current  → shows node, python, rust, go all pinned (not empty)
   g. sysctl iogpu.wired_limit_mb  → returns 122880
   h. ~/1_PROJECTS/mlx-lab/.venv/bin/python -c "import mlx.core as mx; print(mx.metal.is_available())"
      → returns True
3. Ghostty interactive checks (ask user to confirm):
   - cmd+` opens Quick Terminal with slide animation
   - cmd+shift+p opens Command Palette
   - Tab title shows <dirname>  <branch> (not literal command string)
4. Claude Code check: `claude mcp list` shows the ≥4 generic MCPs from Prompt 4
5. Write /tmp/m5-verify-$(date +%Y%m%d).md with PASS/FAIL per check.
   For every FAIL, include the exact remediation command.

DO NOT:
- Install or uninstall anything (this is diagnostic only)
- Edit a config to "fix" a FAIL — surface it, stop, hand off to Prompt 6 (Debugging)
- Trust a green verify.sh alone without running the 8 functional smoke tests
  (verify.sh tests presence, smoke tests test function)
- Run smoke test (c) if no microphone available — `say` works headless but flag it

SUCCESS:
- verify.sh exits 0
- All 8 functional smoke tests pass, OR each FAIL has a precise remediation noted
- /tmp/m5-verify-YYYYMMDD.md is on disk with a final PASS/FAIL tally
- End-of-turn line is either "✅ All green" OR "⚠️ N failures:" + bulleted list

Prompt 6 — Debugging (investigate, don't rebuild)

When something breaks, resist the urge to nuke and rebuild. This prompt is the scientific-method version of "fix it": capture the symptom precisely, form a written hypothesis before changing anything, test the hypothesis with the smallest possible experiment, apply ONE fix at a time, and re-verify. The output is a debug journal at /tmp/m5-debug-*.md that documents the whole loop — useful for future-you and useful if the fix turns out to be wrong.

When to use: Anytime a tool stops working or a verify check flips from green to red. Expected outcome: A root cause in writing, one fix applied, verify.sh is green again. Wall-clock varies — minutes for simple PATH issues, hours for genuine drift.

/goal — A tool is broken. Investigate scientifically. Don't rebuild.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup

LOCKED DECISIONS:
- `setup.sh` is NOT a debugger — running it again overwrites configs.
  Only re-run setup.sh blocks individually after you know what's wrong.
- iogpu.wired_limit_mb sysctl needs sudo — always surface to user, never silent-sudo

RULES:
- One hypothesis at a time. One fix at a time. Re-test between each.
- Mnemosyne: every claim about "it worked before" must be verified against
  the current binary, not against memory. Tag [UNVERIFIED] otherwise.
- Git identity: Adam Boudjemaa <boudjemaa.adam@gmail.com>, no -c overrides
- FRESH machine context: no project repos exist yet

DO:
1. Capture the symptom precisely. Open /tmp/m5-debug-$(date +%Y%m%d-%H%M).md and write:
   - Exact command run
   - Exact error message (stderr verbatim)
   - Exit code
   - Environment: echo $TERM $SHELL, which <bin>, brew --version, uname -a, sw_vers
2. Form a hypothesis BEFORE touching anything. Write in the debug file:
     Hypothesis: <X> is broken because <Y>.
     Evidence: <Z>.
3. Falsify the hypothesis with the smallest possible test. Some lenses:
   - Diff /tmp/verify-*.log between the last-known-good run and now
   - `brew doctor` + `brew config` to surface PATH / arch leakage
   - `cat ~/Library/Logs/Homebrew/<formula>/*.log` for build-time failures
   - `dscl . -read /Users/$USER` for unexpected shell config
   - `launchctl list | grep <name>` if it's a service
   - Run the failing command with `--verbose` or strace-equivalent
   - Bisect via `brew uninstall` + `brew install` ONLY when all other paths are exhausted
4. Document each test + result in the debug file. Number them.
5. Only AFTER the root cause is written down: apply ONE fix.
6. Re-run ~/m5-setup/verify.sh to confirm no regression elsewhere.

DO NOT:
- Run setup.sh as a "general reset" — it rewrites configs and hides drift
- `brew reinstall` without first reading the failure log
- Touch sysctl iogpu.wired_limit_mb without sudo confirmation from the user
- Delete ~/.config/<tool>/ without backing up first (cp <dir> <dir>.bak-$(date +%s))
- Apply more than one fix per test cycle — you'll never know which one worked
- Trust "it worked before" without re-running against the current binary

SUCCESS:
- /tmp/m5-debug-YYYYMMDD-HHMM.md has all five sections:
  Symptom / Hypothesis / Tests / Root cause / Fix
- The fix is reversible (a backup exists, or the change is a single config line)
- verify.sh is green OR cleanly explains the remaining FAIL with a doc reason
- One-paragraph end-of-turn: "Root cause: <X>. Fix: <Y>. Verified by: <Z>."

Prompt 7 — Cleanup + Documentation

Periodic hygiene — two separate jobs. This prompt bundles (1) uninstall cleanup of dropped apps that verify.sh flags as WARN, and (2) doc-sync edits where the gist files have drifted from current reality. Both happen with explicit y/N from the human — no silent uninstalls, no silent gist pushes. After uninstalls, brew cleanup frees disk; after gist edits, gh gist edit pushes each file individually with a commit message describing the drift.

When to use: Monthly hygiene, or right after a verify run that shows new WARNs, or after you've noticed a gist file is wrong. Expected outcome: Zero WARN entries from verify.sh, gist files updated and pushed. Wall-clock ~10-20 minutes (most time is waiting for y/N responses).

/goal — Remove dropped items + sync gist docs with reality.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup
- Files in gist: adam-mac-setup.md, Brewfile, setup.sh, verify.sh, 00-STATUS.md

LOCKED DECISIONS:
- Dropped (do NOT re-add): mstystudio, msty, rectangle, sleeve, wezterm,
  procs, bottom, httpie, spotify, docker (Docker Desktop), cursor, tableplus, gcloud-cli
- VS Code (Continue.dev) replaces Cursor · OrbStack replaces Docker Desktop · Apple Music replaces Spotify
- whisper-cpp replaces openai-whisper

RULES:
- Every uninstall asks for y/N confirmation per item — NEVER batch-uninstall silently
- Mnemosyne: every "drift" claim must show file:line and the contradicting reality
- Git identity: commits to the gist as Adam Boudjemaa <boudjemaa.adam@gmail.com>
- FRESH machine: no project repos in ~/projects yet — leave that path alone

DO:
1. Re-run ~/m5-setup/verify.sh — capture the WARN list and TO_REMOVE candidates
2. For each TO_REMOVE candidate, STOP and surface the list to the human — wait for
   an explicit y/N reply per item. Do NOT batch. Likely candidates:
     brew uninstall --cask mstystudio
     brew uninstall --cask msty
     brew uninstall --cask notion notion-calendar   (dropped 2026-05-27)
     brew uninstall --cask slack signal             (dropped 2026-05-27)
     brew uninstall --cask spotify    (only if user confirms Apple Music is in use)
     brew uninstall --cask docker     (only if OrbStack is present)
     brew uninstall --cask cursor     (VS Code + Continue.dev is the chosen editor)
3. After confirmed uninstalls: `brew cleanup --prune=all` to free disk
4. Cross-check gist files vs current reality:
   - Brewfile: any cask/formula renamed since last edit? (use formulae.brew.sh)
   - adam-mac-setup.md: any Step's tool deprecated, renamed, or replaced?
   - setup.sh: any heredoc writes a config the walkthrough no longer describes?
   - verify.sh: any check that fails-by-design (false positive)?
5. For each drift, edit the file LOCALLY in ~/m5-setup/ then push:
     gh gist edit fad46568b34a09bf453fd48a57123252 -f <filename> ~/m5-setup/<filename>
   (one push per file — gh gist edit accepts one file at a time)
6. After all pushes, re-fetch to confirm: gh gist view fad46568b34a09bf453fd48a57123252 -f <filename> | wc -l

DO NOT:
- brew uninstall ANYTHING without explicit y/N from the user — even items in the
  dropped list (the user may have a current exception in mind)
- `brew cleanup -s` (aggressive — wipes unfinished downloads)
- Delete config files outside ~/.config/<tool>/ paths
- Re-add Rectangle / Spotify / mstystudio / msty / wezterm / Docker Desktop / VS Code
  to the gist files even if you find them used somewhere — the drop is intentional
- Push gist updates with a generic commit message — describe WHAT drift you fixed

SUCCESS:
- ~/m5-setup/verify.sh shows 0 WARN entries
- All 4 gist files reflect current binary reality
- The gist commit log shows precise change descriptions for each file pushed
  (gh gist view fad46568b34a09bf453fd48a57123252 to read)
- One-paragraph end-of-turn: N items removed, M doc drifts patched, K bytes
  freed by `brew cleanup`

Prompt 8 — Final Review (adversarial)

The last line of defense against quietly-broken setups. This prompt is read-only and ruthless: it treats every claim in the gist as suspect, mentally pastes each of Prompts 1-7 into a hypothetical fresh session with zero prior history, and predicts where each one would fail. It reads every line of the four gist files (no skimming), grades findings as BLOCKER / NIT / FYI, and writes a triage report. Run this before declaring "the M5 is done."

When to use: Before declaring setup complete, or after editing any gist file via Prompt 7. Expected outcome: /tmp/m5-review-YYYYMMDD.md with triaged findings + a ship/patch/restart verdict. Takes ~5-10 minutes wall-clock.

/goal — Adversarial review of the gist. Find what's broken or hallucinated.

HARD CONTEXT
- Operator: Adam Boudjemaa <boudjemaa.adam@gmail.com> · GitHub: Aboudjem
- Hardware: MacBook M5 Max 14" 128 GB · macOS Tahoe (26.x) · arm64
- Brand: Neon Noir — #00D4FF · #FF006E · #7C3AED · #0A0A14
- Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252
- Local clone: ~/m5-setup
- Files in gist: adam-mac-setup.md, Brewfile, setup.sh, verify.sh, 00-STATUS.md

LOCKED DECISIONS (anything contradicting these is by definition a BLOCKER):
- Clean install · no Migration Assistant · no project import
- Ghostty + Antidote + Starship + Atuin + mise + uv
- Brave / Chrome / Firefox · VS Code (Continue.dev) · Obsidian (no Notion) · Apple Music
- OrbStack · Ollama · whisper-cpp · Foundry (curl) · sui/supabase in core
- iogpu.wired_limit_mb = 122880
- Dropped: mstystudio, msty, rectangle, sleeve, wezterm, procs, bottom, httpie, spotify, notion, notion-calendar

RULES:
- Read-only — review surfaces problems, does NOT apply fixes
- Be ruthless: every doubt gets a finding, even minor ones
- Mnemosyne: any "common knowledge" claim without a primary source = automatic flag
- Git identity: Adam Boudjemaa <boudjemaa.adam@gmail.com>, no -c overrides
- FRESH machine: assume the reviewer is on a clean M5 with zero history

DO (read-only; you are a skeptic):
1. Mentally paste each of Prompts 1-7 into a fresh Claude Code session with
   ZERO prior conversation history. For each prompt, predict:
   - Does it carry all the context it needs, or does it lean on history?
   - Are every referenced file path and tool name verified to exist?
   - Are all install commands current (no deprecated brew names, no dead URLs)?
   - Is the success criterion BINARY and measurable (not "looks right")?
2. Read every line of the four gist files (~/m5-setup/*):
   - adam-mac-setup.md — every "see Step X" — does Step X actually exist?
   - Brewfile — every cask/formula — fetch `brew info <name>` to confirm current
   - setup.sh — every heredoc + every command — does it run cleanly on Tahoe 26.x?
   - verify.sh — every check — right field number, right syntax, no shadowed builtins?
3. Flag every:
   - Hallucinated command (referenced but doesn't exist)
   - Deprecated package name (brew has renamed it)
   - Missing dependency chain (Tool A needs Tool B installed first, not enforced)
   - Marketing-fluff claim ("premium", "best", "fastest") without a primary source
   - Implicit-context assumption (a next session won't know X)
4. Write /tmp/m5-review-$(date +%Y%m%d).md with sections:
     ## BLOCKER findings (must fix before shipping)
     ## NIT findings (should fix soon)
     ## FYI / annotations
     ## Synthesis — Total count, top 3 load-bearing, verdict (SHIP / PATCH / RESTART)

DO NOT:
- Apply any fix — this prompt is purely review
- Be polite or charitable — every doubt gets a finding
- Trust any claim sourced to "training data" or "common knowledge"
- Skip a file because it "looks fine" — read every line, including comments

SUCCESS:
- /tmp/m5-review-YYYYMMDD.md exists with all 4 sections populated
- Every finding cites file:line precisely
- Each finding rated BLOCKER / NIT / FYI
- End-of-turn: "N BLOCKER · M NIT · K FYI · Verdict: SHIP / PATCH / RESTART"
  + one-paragraph synthesis naming the top 3 load-bearing issues

Important

The walkthrough below is the explained reference. The 8 prompts above are the operational interface. If a prompt mentions "Step X" (e.g. "macOS defaults from setup.sh §9"), that step lives in the walkthrough — never assume the prompt itself contains it.


How to read this

  • 🔍 Each command has an explanation of what it does to your system
  • 📦 Each tool has: what it is + why you want it + what it replaces
  • ⚙️ Each config file is annotated section-by-section
  • ⚠️ Warnings explain the gotchas (drift, breaking changes, kernel panics)

Three patterns you'll see repeated:

  • brew install <name> → downloads + installs a CLI tool (lives in /opt/homebrew/bin)
  • brew install --cask <name> → installs a GUI app (lives in /Applications)
  • defaults write <domain> <key> -<type> <value> → writes a macOS system preference

Before you start — macOS settings explained

Important

Sign in to Apple ID. Turn iCloud Documents & Desktop sync OFF in System Settings → Apple ID → iCloud → Drive. This syncs your entire home folder to the cloud — slow, eats bandwidth, leaks data. FileVault ON for disk encryption.

The defaults command writes macOS preferences directly to plist files instead of clicking through System Settings. Run each block, explanation above each :

🔍 Show hidden files in Finder — macOS hides files starting with . (like .zshrc, .env). You need to see them as a dev.

defaults write com.apple.finder AppleShowAllFiles -bool true

🔍 Show full POSIX path in Finder title bar — instead of "Documents", you see /Users/adamboudj/Documents. Useful for cd-ing where you are.

defaults write com.apple.finder _FXShowPosixPathInTitle -bool true
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true

🔍 Key repeat at maximum speed — when you hold a key, how fast does it repeat? Default is slow. KeyRepeat 2 = ~30ms between repeats. InitialKeyRepeat 15 = hold time before repeat starts (~225ms). Noticeably faster when navigating and editing code.

defaults write -g KeyRepeat -int 2
defaults write -g InitialKeyRepeat -int 15

Tip

The -g flag means "global domain" — applies to all apps, not a specific one.

🔍 Kill smart quotes / dashes / spell-correct — macOS auto-replaces " with " and -- with . This BREAKS code paste. Disable it system-wide.

defaults write -g NSAutomaticDashSubstitutionEnabled -bool false
defaults write -g NSAutomaticQuoteSubstitutionEnabled -bool false
defaults write -g NSAutomaticSpellingCorrectionEnabled -bool false

🔍 Dock that hides instantly — by default the Dock takes 0.5s to show/hide. These commands make it instant.

defaults write com.apple.dock autohide -bool true
defaults write com.apple.dock autohide-delay -float 0
defaults write com.apple.dock autohide-time-modifier -float 0.3
defaults write com.apple.dock show-recents -bool false

🔍 Screenshots to a dedicated folder — by default screenshots dump on Desktop. Make a folder for them, point macOS at it. Hazel will later auto-archive old ones.

mkdir -p ~/Pictures/Screenshots
defaults write com.apple.screencapture location ~/Pictures/Screenshots

🔍 Apply all changesdefaults writes to disk but the running apps don't know. killall restarts them so they re-read the prefs.

killall Finder Dock SystemUIServer

Step 1 — Xcode Command Line Tools (the foundation)

Important

This is the foundation everything else needs. Without it: no compiler, no git, no Python C extensions, no Homebrew.

🔍 What this command does — opens a system popup that downloads Apple's developer toolchain (~2 GB) : git, clang (C/C++ compiler), Python headers, SDK files. NOT the full Xcode IDE (that's 12 GB, you don't need it).

xcode-select --install

Click Install in the popup. Wait ~10 min. After, verify :

git --version            # should print git version 2.39.x (Apple's lagged version — we'll override later)
xcode-select -p          # should print /Library/Developer/CommandLineTools

Step 2 — Homebrew (the package manager)

🔍 What Homebrew isapt for Mac. Without it, installing dev tools means downloading DMGs one by one. With it, you type brew install <name> and it handles download + install + PATH.

🔍 What this curl pipe does — fetches the official install script from GitHub and runs it. The script :

  1. Detects Apple Silicon (M1/M2/M3/M4/M5) → installs to /opt/homebrew
  2. Asks for your sudo password (creates the /opt/homebrew directory, needs root)
  3. Downloads ~500 MB of base tools
  4. Sets up the formula cache
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

At the end, the script prints 3 echo lines. They look like :

echo >> ~/.zprofile 'eval "$(/opt/homebrew/bin/brew shellenv)"'
eval "$(/opt/homebrew/bin/brew shellenv)"

🔍 What those lines do — add Homebrew to your PATH so your shell can find brew. The first one writes it to ~/.zprofile (loaded on login). The second runs it now for this session.

Run them, close the terminal, reopen, then verify :

which brew     # MUST output /opt/homebrew/bin/brew
brew --version

Warning

If which brew returns /usr/local/bin/brew, that's the Intel Mac path running through Rosetta translation. You'd waste 30% of every brew command's speed. Re-run the installer.


Step 3 — Terminal: Ghostty

🔍 Why a new terminal — macOS Terminal.app is fine but slow. Ghostty is GPU-rendered via Metal (your M5 has 40 GPU cores), boots in <100ms, supports all modern terminal features.

brew install --cask ghostty

📦 Ghostty v1.3+ → GPU terminal by Mitchell Hashimoto (the guy who made Vagrant + Terraform). Native AppKit, beautiful defaults. Replaces Terminal.app + iTerm2.

Warning

Known issue : multi-line paste in Claude Code breaks on Ghostty (open Anthropic bug #54700). Workaround in Step 9.

Open Ghostty once, press ⌘+, to see config options. We'll write a config file in Step 21.


Step 4 — Shell: zsh + Antidote + Starship + Atuin

macOS already runs zsh by default. We're adding three things : a plugin manager, a prompt, and a history sync.

🔍 The install command — three brew formulae in one line. Faster than 3 separate calls.

brew install antidote starship atuin

📦 Antidote v2.1.0 → zsh plugin manager. Reads a plain-text list of plugins. Much faster than oh-my-zsh (10% first-prompt lag vs 187%, measured by zsh-bench). Simpler than zinit (no DSL).

📦 Starship v1.25.1 → the part of your shell that shows the prompt (adam@mac ~/projects ❯). Adds git branch, language version, command duration. Renders in 5-15ms (vs Powerlevel10k's 100ms+).

📦 Atuin v18.16.1 → replaces bash/zsh history with an encrypted searchable DB. Press Ctrl+R, get fuzzy search across every command you've ever run. Optionally syncs across machines (your Mac + VPS).

We'll wire them into ~/.zshrc in Step 21. For now they're installed but not active.


Step 5 — Modern CLI tools (replace the 1970s Unix defaults)

🔍 The install command — 27 packages. Brew downloads and links them in parallel, takes ~3 minutes.

brew install eza bat fd ripgrep fzf zoxide \
             lazygit yazi gh jq yq tree \
             htop btop dust duf \
             direnv tldr gnupg \
             mkcert mas xh doggo \
             wget watch imagemagick yt-dlp ncdu
Tool Command What it does Why you want it
eza eza, eza -la Lists files Colored, shows git status (M modified, ?? untracked), nicer than ls
bat bat <file> Prints files with syntax highlighting + line numbers cat with brain
fd fd <name> Finds files by name 3× faster than find, smart defaults, no -name "*.py" syntax
ripgrep rg <pattern> Greps inside files 10× faster than grep, respects .gitignore automatically
fzf Ctrl+R, Ctrl+T Fuzzy finder Interactive picker, pipes into anything
zoxide z <dir> Smart cd Learns directories you visit. z proj jumps to your most-used "proj"
lazygit lazygit Git TUI Stage/commit/branch with keyboard only
yazi yazi File manager TUI Vim-keys, image preview, multi-pane
gh gh pr create GitHub CLI Create PRs, view issues, clone repos from terminal
jq jq '.foo' JSON processor Filter/transform JSON in pipelines (curl ... | jq '.items[]')
yq yq '.foo' Same but YAML For Kubernetes/CI configs
tree tree -L 2 Print directory tree When you need a visual of a folder structure
htop / btop htop, btop Process monitors top with colors, mouse, killable processes (btop = pretty version)
dust dust Disk usage Visual tree of where disk space is going (vs du -sh *)
duf duf Free disk space Replaces df, color tables per filesystem
direnv auto Auto-load .envrc Walk into a folder with .envrc, it auto-loads env vars. Walk out, they vanish. Magic for per-project secrets
tldr tldr <cmd> Short man pages "How do I tar a folder?" → 3 examples instead of 300 lines of man
gnupg gpg GPG keys / encryption Formula name is gnupg, binary is gpg. Used for git commit signing
mkcert mkcert example.local Local HTTPS certs When you need HTTPS in dev (cookies, CORS)
mas mas install 12345 Mac App Store CLI Install paid App Store apps from script
xh xh GET ... Rust version of httpie 10× faster startup
doggo doggo example.com DNS lookup dig modernized (the older dog is dead since 2020)
wget wget <url> Download files Resume support, better than curl for big files
watch watch -n 2 ls Repeat a command Re-runs every N seconds, shows the output
imagemagick magick convert Image processing Resize, convert formats, optimize from terminal
yt-dlp yt-dlp <url> YouTube downloader Maintained fork of youtube-dl
ncdu ncdu Interactive disk usage Walk through folders to see what's eating space

🔍 Activate fzf keybindings — fzf is installed but the shell hooks aren't active. This script wires Ctrl+R (history search), Ctrl+T (find file), Alt+C (jump to subdir).

$(brew --prefix)/opt/fzf/install

Accept all 3 defaults (auto-completion, keybindings, update shell config).


Step 6 — Dev runtimes via mise

🔍 What this installs — three things that make polyglot dev sane :

brew install mise uv git

📦 mise (2026.5.15) → polyglot version manager. Replaces nvm (Node), pyenv (Python), rustup (Rust), gvm (Go), asdf (everything). One tool, one config (~/.config/mise/config.toml). Per-project: drop a .mise.toml in your repo, mise auto-switches versions when you cd into it.

📦 uv (0.11.16) → Python package + virtualenv manager by Astral (the Ruff team). Replaces pip + venv + pipx + poetry. 10-100× faster. Rye was absorbed into uv in Feb 2026.

📦 git → Brew's git stays current (2.54+). Xcode CLT's git lags 6-12 months and breaks new GitHub features. After this brew install, git --version should jump from 2.39 to 2.54+.

🔍 Pin language versions globallymise use --global writes to ~/.config/mise/config.toml. From now on, opening any new shell will have these versions on the PATH.

mise use --global node@lts python@3.13 rust@stable go@latest

🔍 What just happened — mise downloaded the LTS Node (currently 22.x), Python 3.13, the latest stable Rust toolchain, and the latest Go. It built shims in ~/.local/share/mise/shims/ that intercept node, python, rustc, go calls and route to the right version. You'll see them when you run mise activate zsh (we'll add that to ~/.zshrc in Step 21).

🔍 Verify :

mise current
# tools active:
# node       lts        22.11.0
# python     3.13       3.13.0
# rust       stable     1.83.0
# go         latest     1.23.4

Step 7 — Git config

🔍 What git config --global does — writes to ~/.gitconfig. These are the settings every new repo inherits.

git config --global user.name "Adam Boudjemaa"
git config --global user.email "boudjemaa.adam@gmail.com"
git config --global init.defaultBranch main
git config --global pull.rebase false
Setting What it does
user.name / user.email Stamps every commit with these. Required — git refuses to commit without
init.defaultBranch main New repos start with main instead of master
pull.rebase false git pull does a merge by default (not rebase). Some teams prefer rebase — your call

We'll write a full ~/.gitconfig (with SSH commit signing via 1Password and aliases) in Step 21.

🔍 SSH key for GitHub — generates a public/private key pair. The private key stays on your Mac (~/.ssh/id_ed25519). The public key (~/.ssh/id_ed25519.pub) goes to GitHub.

ssh-keygen -t ed25519 -C "boudjemaa.adam@gmail.com"

What it asks :

  1. File location — press Enter (default ~/.ssh/id_ed25519)
  2. Passphrase — press Enter twice for no passphrase (1Password agent will handle this later)

🔍 Copy public key to clipboard :

pbcopy < ~/.ssh/id_ed25519.pub

Then : github.com/settings/keys → New SSH key → paste → Save. Also enable as a signing key (separate option below the SSH key list).

🔍 Test the SSH connection :

ssh -T git@github.com
# Hi Aboudjem! You've successfully authenticated, but GitHub does not provide shell access.

Step 8 — VS Code (the editor)

brew install --cask visual-studio-code

📦 VS Code v1.122+ → uses Microsoft's marketplace (no OpenVSX gaps), Settings Sync via GitHub/Microsoft account. AI lane via Continue.dev extension wired to local Ollama/MLX (config: ~/.continue/config.yaml), so coder models stay 100% local. Cursor was dropped: its cloud-routed model proxy + Chat-only custom model honoring makes it a poor local-LLM fit on this M5 stack.

🔍 code shell command is auto-installed by the cask (linked at /opt/homebrew/bin/code). code . opens the current folder. For a tunnel session: code-tunnel.


Step 9 — Claude Code (CLI agent) + paste fix

🔍 What this installsclaude-code (Anthropic's CLI agent) + vercel (deploys) + pnpm (faster npm). All globals. setup.sh §4 + Brewfile + verify.sh all treat the three as required.

npm install -g @anthropic-ai/claude-code vercel pnpm

📦 Claude Code (2.x) → Anthropic's CLI agent. cd into any project, type claude, ask anything. It reads files, edits them, runs commands, runs tests. Pairs with VS Code's Continue.dev (local Ollama/MLX for autocomplete + chat) and the opencode TUI. Stays on Anthropic cloud via the Claude Max subscription — never repointed to local Ollama (see §22.5).

🔍 First run — opens an OAuth URL in your browser. Sign in with your Anthropic account.

claude

Caution

Ghostty + Claude Code multi-line paste bug is OPEN (Anthropic #54700) as of May 2026. Pasting multi-line text mangles newlines. Workaround : override the TERM env var to xterm-256color (instead of the default xterm-ghostty).

Run this — appends the alias block to your ~/.zshrc in one command :

cat >> ~/.zshrc <<'EOF'

# === Claude Code Ghostty paste workaround (Anthropic #54700) ===
if [ "$TERM_PROGRAM" = "ghostty" ]; then
  alias cc='TERM=xterm-256color claude'
else
  alias cc='claude'
fi
EOF

source ~/.zshrc   # reload so `cc` works immediately

🔍 How cat >> file <<'EOF' ... EOF workscat reads stdin and prints it. >> redirects to the file in append mode (doesn't overwrite existing content). <<'EOF' is a "heredoc" — everything until the next line that says EOF becomes the stdin. The single quotes around 'EOF' mean no variable substitution (so $TERM_PROGRAM stays literal).

🔍 How the alias works$TERM_PROGRAM is set by Ghostty itself when it launches a shell. The if block makes cc an alias that prefixes the right TERM only when you're inside Ghostty. In iTerm2 or Terminal.app, cc just calls claude directly.

🔍 Image paste in Claude Code — screenshot a region with Cmd+Shift+Ctrl+4 (puts image bytes on clipboard, NOT a file path). Then in Claude paste with Ctrl+V not Cmd+V — the terminal app intercepts Cmd+V. Success rate ~60%. 100% reliable fallback : drag the image file from Finder into the Claude window.

🔍 Claude Code env vars — these tune the agent's behavior. All verified live in cli.js v2.1.152 source.

Run this — appends the env vars to ~/.zshrc :

cat >> ~/.zshrc <<'EOF'

# === Claude Code tuning (verified in cli.js v2.1.152) ===
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1   # kills telemetry + survey + non-API calls
export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=15      # bump from default 10 — M5 can handle
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1         # stops "Claude Code" tab title overwrite
export CLAUDE_CODE_EFFORT_LEVEL=xhigh               # max test-time-compute on Opus 4.7
export BASH_DEFAULT_TIMEOUT_MS=300000               # long-running bash tools don't auto-kill
EOF

source ~/.zshrc   # reload to apply

🔍 What each does :

  • DISABLE_NONESSENTIAL_TRAFFIC — Anthropic ships telemetry by default; this kills it for privacy + speed
  • MAX_TOOL_USE_CONCURRENCY=15 — when Claude calls multiple tools in parallel (e.g. read 20 files), it caps at 10 by default; M5 can comfortably do 15
  • DISABLE_TERMINAL_TITLE — by default Claude rewrites your tab title to "Claude Code"; this stops that
  • EFFORT_LEVEL=xhigh — for Opus 4.7, this is the maximum "think hard" knob, makes Claude spend more tokens on reasoning per response
  • BASH_DEFAULT_TIMEOUT_MS=300000 — bash tool calls auto-kill at 5 minutes (instead of the default 2 min)

Full ~/.claude/settings.json template in Step 21.


Step 10 — Browsers

brew install --cask brave-browser google-chrome firefox

📦 Brave (primary) → Chromium-based, same extensions and DevTools as Chrome, but with native ad blocker + tracker blocker + crypto wallet built in (no MetaMask extension needed). Slightly faster page loads, no Google profile tied to your browsing. Best fit for your stack — Polymarket, Web3 audits, daily browsing without ads.

📦 Chrome (backup) → universal DevTools reference + sites that detect/break Brave (Stripe dashboard sometimes, Google Meet edge cases). Open it when something rare doesn't work in Brave.

📦 Firefox → non-Chromium engine. Keep one around for cross-browser testing — if your site renders fine in Brave AND Firefox, it'll work everywhere except Safari (which you already have).

Tip

Optional : brew install --cask zen if you ever want to try the Arc-successor (Firefox fork with workspaces + vertical tabs). Cask is zen, NOT zen-browser. Most people end up keeping Brave as primary.


Step 10b — Brave extensions (curated for Adam's stack)

Note

Brave already blocks ads, trackers, fingerprinting, and includes Brave Wallet built-in — so the list below is only what ADDS value beyond Shields. Skip uBlock Origin (Brave Shields already handles it) and skip MetaMask (Rabby + Brave Wallet cover all chains).

Must-install Day 1 (4 essentials)

Extension What it does Why for you
Obsidian Web Clipper Capture any page → markdown → your Obsidian vault, one keybind ADHD #1 win — capture without context-switch. Official, MIT, local-vault (no cloud)
Rabby Wallet Multi-chain EVM wallet with tx simulation (preview state changes before signing) Audit superpower — 100+ EVM chains, SlowMist + Cure53 audited. Pair with Brave Wallet for routine
Tenderly Dev Toolkit Etherscan overlay: simulate/debug tx in-browser with modified params bounty-lab essential — replay any exploit tx with different state
1Password browser extension Autofill + Quick Access in browser, syncs lock state with desktop You already pay for it via the desktop app — install from 1Password.app → Settings → Browsers → Install (not the Web Store)

ADHD focus (3 picks)

Extension What it does
Unhook for YouTube Hides Shorts, autoplay, recommendations, comments. Strips infinite-scroll dopamine trap. 1M+ users, local-only
Control Panel for Twitter Hides For You feed, trends, "what's happening". Makes X usable for actual signal
ActivityWatch Passive time tracking, 100% local (open-source). Combats ADHD time-blindness. Skip RescueTime (cloud + had CWS delisting scare)

Dev (3 picks)

Extension What it does
React Developer Tools Component tree, props, hooks, Profiler. Required for Vespera/Hazelab/Prevue
Refined GitHub 200+ GitHub UX fixes (better file tree, smart PR actions, fix annoyances). FOSS, weekly releases
Gitako File-tree sidebar for any repo. FOSS, MIT — preferred over Octotree (freemium)

Travel + quality of life (3 picks)

Extension What it does
OneTab Collapses 47 tabs to a list with one click. Direct counter to ADHD tab hoarding
Enhancer for YouTube Picture-in-picture, speed control (0.07-16×), hide recommendations, default quality. Donationware. For podcasts in transit
SponsorBlock Auto-skip YouTube sponsors/intros via crowdsourced timestamps. FOSS. Pair with Enhancer

Specialized (optional, for specific workflows)

Extension When to install
DeepL Translate If you write FR business email often — DeepL beats Google Translate on FR↔EN nuance (Hindi/Thai stick with Brave's built-in Google)
Polyteller Polymarket orderbook + smart-money P&L overlay (your polymarket-agent v2 stack)
Etherscan Extension Gas alerts + address highlight + quick on-chain lookup. Pair with Tenderly
axe DevTools a11y audits for Vespera/Prevue (WCAG, keyboard nav)
Perplexity Sidebar In-page AI Q&A with citations. Replaces "open new tab to Google" loop

Explicit SKIP list

  • uBlock Origin — Brave Shields covers it. Conflict-risk for marginal gain.
  • MetaMask — Rabby + Brave Wallet covers all your chains. Only install if a specific dApp hard-codes MetaMask (rare).
  • Apollo DevTools — you don't ship GraphQL.
  • Mullvad / WireGuard browser ext — Mullvad IPs all blocked by Netflix. Use Tailscale (system-level cask) for nomadic VPN.
  • Forest — cute Pomodoro gamification, ADHD novelty wears off in 2 weeks.
  • RescueTime — cloud-only, outdated, CWS delisting incident 2024. ActivityWatch is strictly better.

Install pattern

Brave shares the Chrome Web Store, so :

  1. Open Brave
  2. Visit each link above
  3. Click "Add to Brave"
  4. Pin the ones you use daily (right-click extension icon → Pin)

Important

Audit each extension's permissions at install. Deny "read all site data" for anything that doesn't need it. Rabby + Tenderly legitimately need broad access (they overlay block explorers). ModHeader / JSON viewers should be scoped to specific origins. Avoid closed-source JSON or header extensions — pick the open-source ones from GitHub-linked publishers.


Step 11 — ADHD power stack

🔍 These are all in brew-core :

brew install --cask raycast jordanbaird-ice obsidian
App What it is Why
Raycast Launcher ⌥+Space, type anything. Replaces Spotlight + Alfred + Maccy + Paste (4 tools in 1)
Ice (jordanbaird-ice) v0.11.12 Menubar declutter Hides Slack/Dropbox/Zoom icons. Free, open-source. Bartender went paid sub + ownership controversy
Hazel Folder rules engine REMOVED 2026-05-30. Replaced by adhd-cleanup.sh + launchd (§21.9, §21.11). Hazel paste-format recipes preserved in §21.9 as a fallback if you want event-driven GUI rules.
Obsidian Markdown notes Files = .md on your disk, no proprietary format. Plugins for backlinks, graph view, Dataview SQL
Cold Turkey Blocker Focus blocker REMOVED 2026-05-30. v4.9 has a persistence bug on macOS 26.5 Tahoe — imports never survive a restart, even after full clean reinstall. Verified via SQLite forensics on /Library/Application Support/Cold Turkey/data-app.db. Replacement TBD.

Tip

After install : open Raycast → Settings → General → Hotkey: ⌥+Space. Then System Settings → Keyboard → Keyboard Shortcuts → Spotlight → uncheck ⌘+Space so they don't fight.


Step 12 — Solidity / Web3 stack (bounty-lab)

🔍 Foundry — NOT installed via brew — the brew formula exists but the canonical install uses the foundryup script for better update cadence.

curl -L https://foundry.paradigm.xyz | bash
source ~/.zshrc
foundryup

🔍 What this does in 3 steps :

  1. curl + bash — downloads the installer script, runs it. It installs the foundryup command (a version manager just for Foundry).
  2. source ~/.zshrc — reload your shell so the new PATH addition (from foundryup install) takes effect.
  3. foundryup — actually downloads Foundry binaries (forge, cast, anvil, chisel) to ~/.foundry/bin/.

📦 Foundry suite :

  • forge — Solidity dev framework (build, test, deploy)
  • cast — Ethereum RPC swiss army knife (cast call, cast send, cast block)
  • anvil — local Ethereum testnet (like Ganache, faster)
  • chisel — Solidity REPL for experiments

🔍 Slither, Halmos, Wake via uv — these are Python tools. uv tool install puts them in ~/.local/bin as standalone binaries (each with its own isolated venv — no dependency conflicts with your project).

uv tool install slither-analyzer
uv tool install halmos
uv tool install eth-wake             # PyPI package name is eth-wake, NOT wake
Tool What it does
Slither Static analysis, ~80 vuln detectors out of the box. slither contracts/MyContract.sol
Halmos Symbolic execution / formal verification. Run on test functions with halmos --function testSomething
eth-wake Test + audit framework by Ackee Blockchain. Property-based testing on Solidity

🔍 Aderyn — brew tap (Cyfrin maintains their own tap) :

brew install cyfrin/tap/aderyn

📦 Aderyn v0.6.8 → Rust-fast static analyzer. Alternative to Slither, sometimes catches things Slither misses. Author = the Cyfrin team (Patrick Collins).

🔍 Echidna fuzzer + Solana/Aptos/Sui :

brew install echidna
brew install solana
brew install aptos
brew install sui   # now in homebrew-core (no tap needed)

📦 Echidna → Haskell-based smart contract fuzzer. Property-based testing — write invariants, Echidna tries to break them with thousands of random inputs. 📦 Solana / Aptos / Sui → CLIs for the non-EVM chains you might audit (Solana = Rust, Aptos+Sui = Move).

Note

sec3 X-Ray (Solana static analyzer) is Docker-only in 2026 — no native macOS binary. Run as : docker run --rm ghcr.io/sec3-product/x-ray:latest.


Step 13 — Cloud / deploy CLIs

🔍 Cloud providers :

brew install awscli                   # formula = awscli (NOT aws-cli — that's a 404)
brew install doctl                    # DigitalOcean CLI

📦 awscliaws s3 ls, aws ec2 describe-instances, etc. After install, run aws configure to set up credentials. 📦 doctl → DigitalOcean. doctl auth init.

🔍 Web platforms (taps required) :

brew install supabase   # now in homebrew-core (no tap needed)
brew install stripe/stripe-cli/stripe

📦 Supabase CLI → DB migrations, auth config, edge function deploy. supabase login, supabase init, supabase db push. 📦 Stripe CLIstripe listen to forward webhooks to localhost during dev, stripe trigger payment_intent.succeeded to test event handlers.

🔍 Tunnels / VPN :

brew install cloudflared
brew install --cask tailscale-app    # cask = tailscale-app (NOT tailscale)

📦 cloudflared → Cloudflare Tunnel. cloudflared tunnel --url localhost:3000 exposes your local dev server at a public HTTPS URL. Free, replaces ngrok (which went enterprise-restrictive). 📦 Tailscale → mesh VPN. Connect your Mac, VPS, and phone into a private network with magic DNS.

🔍 Node packages :

npm install -g vercel pnpm

📦 vercelvercel deploy, vercel logs. Deploys Vespera, Prevue, etc. 📦 pnpm → drop-in for npm, ~2× faster install, content-addressable storage (multiple projects share node_modules on disk).


Step 14 — AI / MLX local stack

🔍 Core LLM tools :

brew install ollama whisper-cpp ffmpeg llama.cpp

📦 Ollama → Local LLM runtime. ollama run huihui_ai/qwen3-abliterated:30b starts an interactive chat.

Caution

Do NOT point the claude CLI at localhost:11434. The claude binary (@anthropic-ai/claude-code) is paid via your Claude Max subscription and must stay on Anthropic cloud. Setting ANTHROPIC_BASE_URL=http://localhost:11434 hijacks the subscription endpoint and silently swaps the cloud model for a local Qwen — you lose Max value AND get degraded quality, with no warning.

For local-model coding, use a separate tool that natively speaks to Ollama: opencode (primary), crush (Charmbracelet, fallback), Continue.dev (VS Code), Cline (VS Code), or Aider. See §22.5 for the local-AI lane setup.

📦 whisper-cpp v1.8.4 → C++ port of OpenAI Whisper. Speech-to-text via Metal GPU.

Caution

Never pip install openai-whisper on Python 3.14. The Python torch MPS backend is broken (no wheels), so it falls back to CPU silently. A 52-min audio file = 60+ min CPU vs 3-5 min with whisper-cpp Metal.

📦 llama.cpp → GGUF model inference, Metal acceleration. Compete with Ollama; usually you only need one. 📦 ffmpeg v8.1.1 → audio/video converter. Required by whisper-cpp (needs 16kHz mono WAV) and by Python AI libs transitively.

🔍 Start Ollama as a background service — so it boots automatically and runs in the background :

brew services start ollama

🔍 What brew services does — uses macOS's launchd to run a daemon. The service file is at /opt/homebrew/Cellar/ollama/.../homebrew.mxcl.ollama.plist. To stop : brew services stop ollama. To list all : brew services list.

🔍 GUI apps :

brew install --cask lm-studio draw-things comfyui
App What
LM Studio v0.4.14 Best daily-driver chat GUI for local LLMs. Browse models, download, chat
Draw Things Image gen GUI (Stable Diffusion, FLUX, Z-Image)
ComfyUI v0.9.3 Node-based image/video workflows. Steep learning curve, max power

🔍 Python MLX stack — per project, NOT global :

mkdir -p ~/1_PROJECTS/mlx-lab && cd ~/1_PROJECTS/mlx-lab
uv init && uv add mlx mlx-lm mlx-lm-lora mlx-vlm mlx-audio mlx-embeddings mflux

🔍 What just happened :

  1. mkdir + cd — created the project folder
  2. uv init — created pyproject.toml and .venv/ in current dir
  3. uv add ... — installed 7 packages into the venv, pinned in pyproject.toml

Why per-project not global : these libs evolve fast. Pinning per-project = reproducible. Global install = breakage when one project needs mlx 0.31 and another needs 0.35.

Package What it does
mlx v0.31.2 Apple's tensor library, like PyTorch but Metal-native
mlx-lm v0.31.3 LLM inference + finetuning CLI : mlx_lm.convert, mlx_lm.lora, mlx_lm.fuse, mlx_lm.generate
mlx-lm-lora v2.1.0 Adds DPO/ORPO/GRPO training on top of mlx-lm (preference learning)
mlx-vlm v0.5.0 Vision-language models (Qwen-VL, etc.)
mlx-audio v0.4.3 TTS + voice clone
mlx-embeddings v0.1.0 Embedding models
mflux v0.17.5 FLUX image generation on MLX (filipstrand)

Note

vllm-mlx v0.3.0 (PyPI, repo waybarrios/vllm-mlx) is the chosen MLX OpenAI-compatible serving backend for the local-AI lane — see §22.5. Launch with vllm-mlx serve <model> --port … (NOT python -m vllm_mlx.server). Keep mlx-lm for finetuning/convert; Ollama remains the GGUF convenience backend.

🔍 Download whisper model — beware HF LFS truncation :

mkdir -p ~/.cache/whisper-cpp
curl -L --fail --retry 3 -o ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin \
  "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin?download=true"
ls -lh ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin

🔍 What each flag does :

  • mkdir -p — create the dir, no error if it exists
  • curl -L — follow HTTP redirects (HF redirects to LFS storage)
  • --fail — exit non-zero on HTTP errors (prevents writing an error HTML page as if it were the model)
  • --retry 3 — retry up to 3 times on network failures
  • -o <path> — output to this file

⚠️ The ls -lh at the end is critical — HuggingFace LFS sometimes truncates downloads silently (curl exits 0, you get a partial file that looks valid). Verify size = ~1.6 GB. If it's smaller, delete and re-download.

🔍 Pull base LLM models (overnight, ~131 GB total) — Adam runs the uncensored/abliterated equivalents of the qwen3 set:

ollama pull huihui_ai/qwen3-abliterated:30b                          # daily driver text
ollama pull huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0  # coding base
ollama pull nomic-embed-text                                         # RAG embeddings (small, ~280 MB)
ollama pull huihui_ai/qwen3.5-abliterated:122B                       # flagship reasoning (~81 GB)

🔍 What ollama pull does — downloads the model weights to ~/.ollama/models/, splits across blobs to dedupe shared layers. The 122B model alone is ~81 GB — go to sleep, it'll be ready in the morning.


Step 15 — Docker: OrbStack

brew install --cask orbstack

📦 OrbStack → fast Docker for Mac. Uses 10× less RAM than Docker Desktop, boots in 2 seconds, native ARM, k8s built-in. docker CLI works exactly the same — apps don't know which runtime is behind.

Caution

Don't install Docker Desktop. Strictly worse than OrbStack in 2026. And in a corp setting, Docker Desktop now requires a Pro license for companies >250 employees — OrbStack is free.


Step 16 — Productivity apps

brew install --cask cleanshot \
             1password 1password-cli \
             postman
App What it does
CleanShot X Screenshots, screen recording, OCR, annotations. Replaces native screenshot (Cmd+Shift+5) with way more features
1Password Password manager, browser extension auto-fills
1Password CLI (op) Inject secrets into scripts without plaintext env vars : op item get 'GitHub' --field token
Postman v12.12 API testing GUI. Optional — xh in terminal covers most needs

Note

Rectangle is an optional window-snapping tool. If you want one-off snap moves : brew install --cask rectangle (optional).


Step 17 — Communications

brew install --cask whatsapp telegram zoom
  • WhatsApp Desktop — native, syncs from phone
  • Telegram — secondary messaging
  • Zoom — video meetings

Step 18 — System utilities (quality of life)

brew install --cask alt-tab stats appcleaner the-unarchiver \
             iina voiceink
App What it does
AltTab v11.1 Windows-style ⌥+Tab with window previews. Cmd+Tab only switches apps — AltTab switches individual windows
Stats v2.12 Menubar system monitor (CPU/RAM/GPU/network/fan). Free, open source
AppCleaner Drag any app onto it → removes app + all preference/cache files. Better than dragging to Trash
The Unarchiver Opens .rar, .7z, .tar.xz, things macOS native can't
IINA Modern video player, all codecs. Replaces QuickTime + VLC
VoiceInk MLX-based dictation. Press a hotkey, talk, get text. Runs Whisper-Large-v3-Turbo on M5 GPU
Shottr REMOVED 2026-05-30 — CleanShot expanded shortcuts cover all use cases (window/area/fullscreen/OCR/recording).

Note

Highlights (PDF annot → Obsidian export) is NOT on brew. Download from highlightsapp.net or App Store.

Music — Adam uses Apple Music (native macOS app, no install). If you ever switch to Spotify: brew install --cask spotify.


Step 19 — M5-specific tuning

🔍 GPU memory cap — by default macOS reserves only ~75% of unified RAM for the GPU. For MLX workloads on a 128 GB machine, you want 120 GB. This sysctl raises the cap.

sudo sysctl iogpu.wired_limit_mb=122880

🔍 What this doesiogpu.wired_limit_mb is a kernel parameter that caps how much RAM the IOGPU subsystem can wire (lock) for GPU use. 122880 MB = 120 GB. Leaving 8 GB for macOS itself.

Caution

Don't push to 124000+ — kernel panic at the next big batch. 122880 is the sweet spot validated empirically by the MLX community.

🔍 Make it permanent (survives reboot) :

# Modern macOS (Tahoe 26.x) does NOT auto-load /etc/sysctl.conf at boot.
# The canonical persistence path is a LaunchDaemon — written by setup.sh §13 to
# /Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist
# (verify presence with: ls -la /Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist)

🔍 What tee -a doestee writes stdin to both stdout AND the file. -a means append. sudo is needed because /etc/sysctl.conf is owned by root.

🔍 MLX zsh functions — wrap MLX_METAL_FAST_SYNCH=1 + caffeinate so you can enable "power mode" before training. Run this :

cat >> ~/.zshrc <<'EOF'

# === MLX max-power mode (M5 Max) ===
mlx_max() {
  export MLX_METAL_FAST_SYNCH=1
  defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
  caffeinate -di &
  echo "MLX max: fast sync ON, sleep OFF, caffeinate"
}
mlx_revert() {
  unset MLX_METAL_FAST_SYNCH
  defaults delete NSGlobalDomain NSAppSleepDisabled
  pkill caffeinate 2>/dev/null
  echo "MLX max reverted"
}
EOF

source ~/.zshrc

Now mlx_max toggles power mode for long training runs, mlx_revert restores normal.


Step 20 — Cask renames you'd otherwise miss

Warning

Verified live on formulae.brew.sh May 2026. These are the gotchas where the obvious name 404s :

❌ Wrong ✅ Correct Why
brew install --cask zen-browser brew install --cask zen (optional) Cask is just zen — but Brave is now the primary pick
brew install --cask tailscale brew install --cask tailscale-app Renamed
brew install --cask msty brew install --cask mstystudio Renamed
brew install aws-cli brew install awscli Formula has no hyphen
brew install rustup-init brew install rustup Renamed
brew install gpg brew install gnupg Formula = gnupg, binary = gpg
pip install wake uv tool install eth-wake PyPI name disambiguation
brew install --cask wezterm Stuck Feb 2024 — drop Abandoned
brew install dog Use doggo or stock dig Dead since 2020
brew install --cask pinokio Manual DMG from desktop.pinokio.co No cask
brew install --cask highlights Direct download highlightsapp.net No cask

Hazel IS on brew now (cask hazel v6.1.2) — you don't need to download from noodlesoft.com anymore (still needs $42 license at first launch).


Step 21 — Config files (paste-ready, with section commentary)

21.1 — ~/.zshrc (full template)

Save the file with code ~/.zshrc (after Step 8) or nano ~/.zshrc. This is the file zsh runs at every shell startup.

# ============================================================
#  ~/.zshrc — M5 Max, May 2026
# ============================================================

# ----- PATH: where the shell looks for binaries -----
# Homebrew puts binaries in /opt/homebrew/bin (Apple Silicon)
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"
# rustup's binaries live in a separate prefix
export PATH="$(brew --prefix rustup)/bin:$PATH"
# uv puts its installed tools (slither, halmos, etc.) here
export PATH="$HOME/.local/bin:$PATH"

# ----- Antidote: zsh plugin manager -----
# Loads the plugins listed below, lazily, on shell startup.
# Each line in the heredoc is "owner/repo".
source $(brew --prefix)/opt/antidote/share/antidote/antidote.zsh
antidote load << 'EOF'
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-completions
zdharma-continuum/fast-syntax-highlighting
EOF

# ----- Prompt + interactive tools -----
# starship renders the prompt (the part before the cursor)
eval "$(starship init zsh)"
# zoxide lets `z foo` jump to your most-used "foo" directory
eval "$(zoxide init zsh)"
# atuin replaces Ctrl+R with a fuzzy DB-backed history search
eval "$(atuin init zsh)"
# mise auto-switches language versions per project
eval "$(mise activate zsh)"
# direnv auto-loads/unloads .envrc files when you cd
eval "$(direnv hook zsh)"
# fzf keybindings (Ctrl+R history, Ctrl+T file, Alt+C dir)
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

# ----- Claude Code (Ghostty paste workaround for issue #54700) -----
# Ghostty advertises kitty-protocol terminfo. Claude Code's paste
# tokenizer mishandles it on multi-line paste. Override TERM only
# when running Claude inside Ghostty.
if [ "$TERM_PROGRAM" = "ghostty" ]; then
  alias cc='TERM=xterm-256color claude'
else
  alias cc='claude'
fi
# Tuning (all env vars verified in cli.js v2.1.152 source)
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=15
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1
export CLAUDE_CODE_EFFORT_LEVEL=xhigh
export BASH_DEFAULT_TIMEOUT_MS=300000

# NOTE: do NOT set ANTHROPIC_BASE_URL — the `claude` CLI must stay on Anthropic cloud
# (Claude Max subscription). Local-model coding uses a SEPARATE tool (opencode primary,
# Crush fallback) wired to Ollama / the MLX backend via that tool's own config. See §22.5 for setup.

# ----- HF token via 1Password (never plaintext) -----
# Requires 1Password app + CLI signed in
# export HF_TOKEN="$(op item get 'HuggingFace' --field credential 2>/dev/null)"

# ----- Aliases: replace the old Unix commands with modern equivalents -----
alias ls='eza --icons --git'
alias ll='eza -la --icons --git'
alias lt='eza --tree --level=2 --icons'
alias cat='bat --paging=never'
alias grep='rg'
alias find='fd'
alias top='btop'
alias du='dust'
alias df='duf'

# ----- Git shortcuts -----
alias g='git'
alias lg='lazygit'
alias gst='git status'
alias gco='git checkout'
alias gp='git push'
alias gpl='git pull'

# ----- Project shortcuts -----
alias dev='cd ~/1_PROJECTS'
alias bl='cd ~/projects/bounty-lab'
alias kb='cd ~/2_KNOWLEDGE'

# ----- MLX max-power functions (M5 Max) -----
# Toggle "give MLX everything" mode before training.
mlx_max() {
  export MLX_METAL_FAST_SYNCH=1
  defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
  caffeinate -di &
  echo "MLX max: fast sync ON, sleep OFF, caffeinate"
}
mlx_revert() {
  unset MLX_METAL_FAST_SYNCH
  defaults delete NSGlobalDomain NSAppSleepDisabled
  pkill caffeinate 2>/dev/null
  echo "MLX max reverted"
}

# ----- fzf theme: Neon Noir -----
# Colors match your design system (#00D4FF cyan, #FF006E magenta, #7C3AED violet)
export FZF_DEFAULT_OPTS="
  --color=bg+:#15151F,bg:#0A0A14,spinner:#FF006E,hl:#00D4FF
  --color=fg:#F5F5F7,header:#00D4FF,info:#7C3AED,pointer:#FF006E
  --color=marker:#34D399,fg+:#F5F5F7,prompt:#00D4FF,hl+:#FF006E
"

# ----- End of ~/.zshrc -----

After saving : source ~/.zshrc to apply. New prompt + plugins should activate.

21.2 — ~/.config/starship.toml

🔍 What this file does — controls how your prompt looks. The format string defines the segments order. Each [segment] block configures one piece.

"$schema" = "https://starship.rs/config-schema.json"

# Order of segments in the prompt
format = """
[](#7C3AED)\
$os\
$username\
[](bg:#0A0A14 fg:#7C3AED)\
$directory\
[](fg:#0A0A14 bg:#0A0A14)\
$git_branch\
$git_status\
[](fg:#0A0A14 bg:#0A0A14)\
$nodejs\
$python\
$rust\
$golang\
[](fg:#0A0A14 bg:#15151F)\
$time\
[ ](fg:#15151F)\
$line_break$character"""

palette = "neon_noir"

[palettes.neon_noir]
cyan = "#00D4FF"
magenta = "#FF006E"
violet = "#7C3AED"
green = "#34D399"
dark = "#0A0A14"
surface = "#15151F"

[character]
success_symbol = "[❯](bold cyan)"
error_symbol = "[❯](bold magenta)"

[directory]
style = "bold cyan"
truncation_length = 3   # show last 3 path segments
truncate_to_repo = true # truncate to git root if inside a repo

[git_branch]
style = "bold magenta"
symbol = " "

[git_status]
style = "bold magenta"

[time]
disabled = false
format = '[ $time ]($style)'
style = "fg:#71717A"
time_format = "%H:%M"

[cmd_duration]
min_time = 500  # only show duration if cmd took >500ms
format = "[took $duration]($style) "
style = "fg:#FBBF24"

21.3 — ~/.config/ghostty/config

🔍 Ghostty's config file formatkey = value pairs, no quotes around strings, comments with #. setup.sh writes this file verbatim. Theme is cyberdream (closest to Neon Noir brand) with a custom Neon Noir palette fallback at ~/.config/ghostty/themes/neon-noir (Section 21.4b).

Note

Font dependency — this config requires JetBrainsMono Nerd Font (brew install --cask font-jetbrains-mono-nerd-font, included in Brewfile + setup.sh §7). Ghostty falls back gracefully if missing, but the icon glyphs won't render in eza/starship.

# Neon Noir — Liquid Glass on macOS Tahoe (26.x)
# Translucent design matching brand: #00D4FF / #FF006E / #7C3AED / #0A0A14
# Docs: https://ghostty.org/docs/config/

# === Theme (cyberdream = closest match to Neon Noir; falls back to neon-noir custom palette) ===
theme = cyberdream

# === Liquid Glass (macOS 26+) ===
background-opacity = 0.85
background-blur = macos-glass-regular
macos-titlebar-style = transparent
macos-icon = retro

# === Window ===
window-decoration = auto
window-padding-x = 14
window-padding-y = 12
window-padding-balance = true
window-vsync = true
window-save-state = always
window-colorspace = display-p3

# === Font (requires font-jetbrains-mono-nerd-font cask) ===
font-family = JetBrainsMono Nerd Font
font-size = 14
font-thicken = true
font-feature = +liga
font-feature = +calt
font-feature = +ss01
font-feature = +zero
adjust-cell-height = 15%

# === Cursor + mouse ===
cursor-style = bar
cursor-style-blink = true
mouse-hide-while-typing = true

# === Shell + behavior ===
shell-integration = detect
shell-integration-features = cursor,sudo,title
copy-on-select = clipboard
clipboard-read = allow
clipboard-write = allow
clipboard-paste-protection = true
confirm-close-surface = false
unfocused-split-opacity = 0.85

# === Neon Noir accents (paint cyan/magenta/violet OVER cyberdream) ===
cursor-color = #00D4FF
selection-background = #7C3AED
selection-foreground = #FFFFFF

# === Keybinds ===
keybind = cmd+t=new_tab
keybind = cmd+d=new_split:right
keybind = cmd+shift+d=new_split:down
keybind = cmd+w=close_surface
keybind = cmd+shift+enter=toggle_split_zoom
keybind = cmd+enter=toggle_fullscreen

# === v2 polish (Scout A, 2026-05-27) — 10 verified items, sourced to ghostty.org/docs ===

# Quick Terminal — Quake-style drop-down (cmd+`)
keybind = global:cmd+grave_accent=toggle_quick_terminal
quick-terminal-position = top
quick-terminal-animation-duration = 0.18
quick-terminal-screen = main
quick-terminal-autohide = true

# Command Palette (cmd+shift+p) — VS Code muscle memory
keybind = cmd+shift+p=toggle_command_palette

# Dynamic tab title: CWD basename + git branch
title-command = printf '%s  %s' "$(basename "$PWD")" "$(git branch --show-current 2>/dev/null)"

# Bell: visual + soft audio, no dock-bounce (1.3.0)
bell-features = visual,audio,no-system
bell-audio-volume = 0.4

# Notify on long-running commands when unfocused (1.3.0)
notify-on-command-finish = unfocused
notify-on-command-finish-action = notify
notify-on-command-finish-after = 10s

# Granular working-directory inheritance (1.3.0)
window-inherit-working-directory = true
tab-inherit-working-directory = true
split-inherit-working-directory = true

# Native Tahoe overlay scrollbar (1.3.0)
scrollbar = system

# Left option = Alt (tmux/helix unlock); right option free for français accents
macos-option-as-alt = left

# Brand-color unfocused-split fill (replaces alpha hack with #0A0A14 overlay)
unfocused-split-fill = #0A0A14

# Resize/padding/blending polish trio
resize-overlay = after-first
window-padding-color = extend
alpha-blending = native

21.4b — ~/.config/ghostty/themes/neon-noir (custom palette fallback)

🔍 Why this exists — if cyberdream is ever removed from Ghostty's bundled themes (or if you want to switch to a pure brand palette), set theme = neon-noir instead. setup.sh writes this file alongside the main config.

background = 0A0A14
foreground = E0E0FF
cursor-color = 00D4FF
selection-background = 7C3AED
selection-foreground = FFFFFF
palette = 0=#0A0A14
palette = 1=#FF006E
palette = 2=#34D399
palette = 3=#FBBF24
palette = 4=#7C3AED
palette = 5=#FF006E
palette = 6=#00D4FF
palette = 7=#F5F5F7
palette = 8=#71717A
palette = 9=#FF4D8F
palette = 10=#6EE7B7
palette = 11=#FCD34D
palette = 12=#A78BFA
palette = 13=#FF6BA8
palette = 14=#7DF0FF
palette = 15=#FFFFFF

21.5 — ~/.config/mise/config.toml

🔍 What this pins — your global default language versions. Any project without its own .mise.toml uses these.

[tools]
node = "lts"
python = "3.13"
rust = "stable"
go = "latest"

[settings]
experimental = true
# Only auto-activate .mise.toml in these trusted directories
trusted_config_paths = ["~/projects", "~/1_PROJECTS"]

21.6 — ~/.gitconfig

🔍 What's special here — SSH commit signing via 1Password. Instead of fiddling with GPG, you sign commits with your SSH key, signed by the 1Password ssh-agent. GitHub verifies it.

[user]
    name = Adam Boudjemaa
    email = boudjemaa.adam@gmail.com
    signingkey = ~/.ssh/id_ed25519.pub

[gpg]
    format = ssh
[gpg "ssh"]
    program = /Applications/1Password.app/Contents/MacOS/op-ssh-sign

[commit]
    gpgsign = true   # auto-sign every commit

[init]
    defaultBranch = main

[pull]
    rebase = false   # merge by default; switch to true if you prefer linear history

[push]
    autoSetupRemote = true   # `git push` works for new branches without --set-upstream
    default = current

[fetch]
    prune = true   # remove local refs to deleted remote branches

[merge]
    conflictstyle = zdiff3   # show common ancestor in conflict markers (clearer)

[diff]
    algorithm = histogram   # smarter than default myers
    colorMoved = default    # highlight moved blocks

[color]
    ui = auto

# Useful aliases — type `git lg` for a pretty graph, `git st` for status, etc.
[alias]
    st = status
    co = checkout
    br = branch
    cm = commit -m
    lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
    last = log -1 HEAD
    unstage = reset HEAD --
    amend = commit --amend --no-edit
    sw = switch
    ll = log --oneline -20

21.7 — ~/.config/atuin/config.toml

🔍 What Atuin replaces — your shell history. By default zsh keeps the last N commands in ~/.zsh_history (plain text). Atuin stores them in SQLite, encrypts, optionally syncs across machines.

# Atuin shell history — encrypted DB + sync
# Docs: https://docs.atuin.sh/

# When you press Enter on a history item, preview it first (don't auto-execute)
# Press Enter again to run. Safer.
enter_accept = false

# Search across ALL your hosts (not just this machine)
filter_mode = "global"
filter_mode_shell_up_key_arrow = "session"

style = "compact"
inline_height = 20
show_preview = true
show_help = true

# Sync every 10 min if you use atuin.sh server (or self-host)
auto_sync = true
sync_frequency = "10m"

[stats]
common_prefix = ["sudo", "time"]

21.8 — ~/.claude/settings.json

🔍 What this controls — Claude Code's default model, env vars, permission policy, file-checkpointing for /rewind, parallel-tool throughput. Live template at template-claude-settings.json in this gist.

{
  "cleanupPeriodDays": 30,
  "env": {
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
    "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "32",
    "CLAUDE_CODE_EFFORT_LEVEL": "xhigh",
    "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
  },
  "permissions": {
    "allow": ["Bash", "Read", "Edit", "Write", "Grep", "Glob"],
    "deny": [
      "Bash(rm -rf /*)",
      "Bash(cat /.env*)",
      "Bash(cat ~/.ssh/*)",
      "Bash(cat ~/.aws/*)"
    ],
    "ask": [
      "Bash(git push --force*)",
      "Bash(rm -rf*)",
      "Bash(npm publish*)"
    ]
  },
  "model": "opus",
  "effortLevel": "xhigh",
  "skipDangerousModePermissionPrompt": true,
  "skipWorkflowUsageWarning": true,
  "theme": "dark",
  "fileCheckpointingEnabled": true,
  "disableDeepLinkRegistration": "disable"
}

🔍 Field by field :

  • model: "opus"family alias, auto-tracks the latest Opus release. When Anthropic ships Opus 5.0, your sessions pick it up automatically. Same trick works for "sonnet" / "haiku".
  • effortLevel: "xhigh" — deep reasoning by default. The SAFE half of "ultracode" without the auto-orchestration runaway risk.
  • env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY: 32 — let Claude fire up to 32 parallel tool calls in one turn. M5 Max (18 cores, 128 GB) handles this easily.
  • permissions.allow — fast-path tools that never need confirmation
  • permissions.deny — hard blocks even with bypass mode
  • permissions.ask — speed bumps for destructive ops (only effective when bypass mode is OFF)
  • fileCheckpointingEnabled — snapshot files before edits so /rewind can restore. Bounded by the §21.12 disk-guard.
  • skipDangerousModePermissionPrompt — accept the bypass-mode dialog up-front (paired with the claude zsh wrapper in §21.1)
  • disableDeepLinkRegistration — don't register the claude-cli:// protocol with macOS

🔍 What's deliberately NOT here: "ultracode": true, "autoCompactWindow", "showThinkingSummaries", "fastMode". These are either session-scoped (silently dropped from settings.json) or buggy / cost-multipliers. Ultracode is forced via the claude zsh wrapper instead (see template-zshrc-claude-wrappers.zsh).

21.9 — ADHD cleanup (Hazel-equivalent via launchd)

🔍 Hazel was uninstalled on 2026-05-30 in favor of a fully scripted equivalent. The launchd version mirrors what Hazel would do for the 13 rules below, runs every 10 minutes, and adds a content-classification tier (OCR + Ollama) on top — see §21.11. Hazel itself is excellent if you want event-driven rules + macOS notifications; rebuild via these recipes if you ever go back.

Recipes (paste into Hazel UI if you choose to use it):

  1. DMGs auto-trash — Folder ~/Downloads. If Kind is Application bundle (dmg) AND Date Added before 1 day agoMove to Trash.
  2. Invoice classify — Folder ~/Downloads. If Name contains invoice OR facture AND Extension is pdfMove to ~/Documents/3_admin/factures/.
  3. Screenshot archive — Folder ~/Pictures/Screenshots. If Date Added before 30 days agoMove to ~/Pictures/Screenshots/archive/.
  4. Audit PDFs to KB — Folder ~/Downloads. If Name matches audit* AND Extension is pdfMove to ~/2_KNOWLEDGE/audits/incoming/.
  5. Empty Trash weekly — Folder ~/.Trash. If Date Trashed before 7 days agoDelete forever.

The scripted version implements 13 rules across 5 folders (Downloads / Desktop / Screenshots / 0_inbox / .Trash) — see adhd-cleanup.sh + com.adam.adhd-cleanup.plist.

21.10 — launchd plist (weekly tidy)

🔍 What launchd is — macOS's native cron + service manager. To run something on schedule, you drop a .plist (XML config) in ~/Library/LaunchAgents/.

Save as ~/Library/LaunchAgents/com.adam.tidy.plist :

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.adam.tidy</string>

    <key>ProgramArguments</key>
    <array>
        <string>/bin/zsh</string>
        <string>-c</string>
        <string>brew cleanup; rm -rf ~/Library/Caches/* ~/.npm/_cacache; docker system prune -af 2>/dev/null</string>
    </array>

    <key>StartCalendarInterval</key>
    <dict>
        <key>Weekday</key>
        <integer>0</integer>
        <key>Hour</key>
        <integer>3</integer>
    </dict>

    <key>StandardOutPath</key>
    <string>/tmp/tidy.log</string>
    <key>StandardErrorPath</key>
    <string>/tmp/tidy.err</string>
</dict>
</plist>

🔍 Activate :

launchctl load ~/Library/LaunchAgents/com.adam.tidy.plist

🔍 What this runs every Sunday at 3 AMbrew cleanup (purges old formula versions), rm -rf ~/Library/Caches/* (system caches), ~/.npm/_cacache (npm cache), docker system prune (dangling containers/images). Logs go to /tmp/tidy.log.

21.11 — Personal launchd-managed scripts (live templates in this gist)

🔍 What's here — three scripts wired to user LaunchAgents. All files live at the root of this gist. On a fresh restore, after setup.sh runs, copy them to their canonical locations and launchctl bootstrap each plist.

Gist file Destination Schedule Purpose
adhd-cleanup.sh ~/.local-bin/adhd-cleanup.sh (chmod 755) every 10 min via com.adam.adhd-cleanup.plist Sweeps Downloads/Desktop/Screenshots/0_inbox/.Trash per 13 name+age rules, then runs Tier C content classification on unmatched PDFs/images/text
classify-file.py ~/.local-bin/classify-file.py (chmod 755) invoked by adhd-cleanup.sh OCR (pdftotext for text PDFs, ocrmac for images / scanned PDFs) → Ollama qwen3-coder-abliterated:30b with format=json → returns {category, name, confidence} → caller auto-routes
claude-file-history-guard.sh ~/.local-bin/claude-file-history-guard.sh (chmod 755) weekly Sun 03:15 via com.adam.claude-file-history-guard.plist Caps ~/.claude/file-history at 2 GB (configurable). Prunes oldest checkpoint dirs first; posts macOS notification on prune. Defends against Claude Code Issue #10107 cascade.
com.adam.adhd-cleanup.plist ~/Library/LaunchAgents/com.adam.adhd-cleanup.plist LaunchAgent for the cleanup
com.adam.claude-file-history-guard.plist ~/Library/LaunchAgents/com.adam.claude-file-history-guard.plist LaunchAgent for the file-history guard

🔍 Required system packages (installed by setup.sh already, listed for clarity):

  • brew install popplerpdftotext for text-PDF extraction
  • pip3 install --break-system-packages ocrmac — macOS Vision OCR for images / scanned PDFs (pulls pyobjc-framework-Vision)
  • Ollama running on :11434 with huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0 pulled

🔍 Install all three on a restore:

mkdir -p ~/.local-bin ~/.local-ai/logs
cp ~/m5-setup/adhd-cleanup.sh ~/.local-bin/
cp ~/m5-setup/classify-file.py ~/.local-bin/
cp ~/m5-setup/claude-file-history-guard.sh ~/.local-bin/
chmod +x ~/.local-bin/{adhd-cleanup,claude-file-history-guard}.sh ~/.local-bin/classify-file.py
cp ~/m5-setup/com.adam.adhd-cleanup.plist ~/Library/LaunchAgents/
cp ~/m5-setup/com.adam.claude-file-history-guard.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.adam.adhd-cleanup.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.adam.claude-file-history-guard.plist

🔍 Sanity check after install:

launchctl list | grep com.adam   # expect adhd-cleanup + claude-file-history-guard (+ any others you maintain)
DRY_RUN=1 zsh ~/.local-bin/adhd-cleanup.sh && tail -5 ~/.local-ai/logs/adhd-cleanup.log
~/.local-bin/classify-file.py ~/Downloads/some.pdf 2>&1 | head -3   # smoke test
zsh ~/.local-bin/claude-file-history-guard.sh && tail -5 ~/.local-ai/logs/claude-file-history-guard.log

🔍 Templated settings (also in this gist):

  • template-claude-settings.json~/.claude/settings.json (see §21.8 for the rendered keys)
  • template-vscode-settings.json~/Library/Application Support/Code/User/settings.json (Solidity Foundry config + Todo Tree ripgrep path fix)
  • template-zshrc-claude-wrappers.zsh — paste this block into ~/.zshrc to force ultracode + --dangerously-skip-permissions on every claude / cc launch (escape hatches: \claude or command claude)

Step 22 — Verify everything

# Foundation
brew doctor                          # "Your system is ready to brew." = green
git --version                        # should be 2.54+ (brew's, not 2.39 Xcode)
mise current                         # node/python/rust/go versions

# AI stack
ollama list                          # list installed models
whisper-cli --version
python -c "import mlx.core as mx; print('MLX Metal:', mx.metal.is_available())"
# → MLX Metal: True

# Solidity
forge --version
slither --version
halmos --version
aderyn --version

# Modern CLI
eza --version
rg --version
fzf --version

echo "✅ all green if you see versions above"

🔍 Day-1 smoke test — Ollama is up — sanity check the local LLM runtime via its NATIVE API (the Anthropic-compat shim is reserved for tools that opt-in; claude MUST NOT be one of them):

curl http://localhost:11434/api/tags          # → JSON list of installed models
curl http://localhost:11434/api/generate \
  -d '{"model":"huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0","prompt":"hello","stream":false}'

🔍 What this proves — Ollama is running, has Qwen3-Coder pulled, and returns a valid response. If the second call returns JSON with a "response" field, everything works.

Caution

Do NOT set ANTHROPIC_BASE_URL to point claude at Ollama. The official claude CLI is paid via your Claude Max subscription and must stay on Anthropic cloud. Setting that env var hijacks the subscription endpoint and silently degrades quality. Verify the var is empty: echo "$ANTHROPIC_BASE_URL" should print nothing.

🔍 Drive Ollama from opencode (primary local-AI CLI) — see §22.5 for full setup:

# opencode reads ./opencode.json or ~/.config/opencode/opencode.json
# Provider block points at Ollama's OpenAI-compat endpoint (NOT the /v1/messages shim)
# Then run opencode interactively or:
opencode -p "list 3 Solidity reentrancy patterns" --model ollama/huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0

🔍 MLX finetune sanity (5 min wall-clock) :

mlx_lm.convert \
  --hf-path Qwen/Qwen3-Coder-7B-Instruct \
  -q --q-bits 4 --q-group-size 64 \
  --mlx-path ~/models/qwen3-coder-7b-q4

🔍 What this does — downloads Qwen3-Coder-7B from HuggingFace (~14 GB bf16), converts to MLX Q4 format (compressed to ~4.5 GB), saves to ~/models/. If this completes, your full finetune pipeline will work.


Step 22.5 — Local-AI lane (post-base setup)

Caution

The claude CLI stays on Anthropic cloud — always. Your Claude Max subscription pays for that endpoint. NEVER set ANTHROPIC_BASE_URL in your shell or any config. Local-model coding uses a separate tool, wired to Ollama via its own provider config.

The local-AI lane is its own world: Ollama runs the language models, and a companion tool gives you a Claude-Code-like UX against those local models. Adam's pick set after research (see scout reports at /tmp/m5-local-tool-picks.md + /tmp/m5-multimodal-local.md):

Lane Tool What it does Wire-up
LLM perf backend (NEW) vllm-mlx OpenAI server :1234 Headless MLX server → M5 Neural Accelerators give up to 4× prefill vs M4 (needs MLX ≥0.30; have 0.31.2). Continuous batching + prefix cache. Serves an abliterated Qwen3-Coder MoE in MLX 4-bit. uv venv ~/.venvs/vllm-mlx --python 3.13 && uv pip install --python ~/.venvs/vllm-mlx/bin/python vllm-mlx hf_xet; serve cmd below
LLM convenience backend Ollama :11434 GGUF abliterated models, always-warm; flash-attn + q8 KV cache already installed; OLLAMA_KEEP_ALIVE=-1 + OLLAMA_CONTEXT_LENGTH=16384 set in its LaunchAgent plist
Primary local CLI opencode (org renamed sst→**anomalyco**) TUI agentic, most Claude-Code-like UX, MIT curl -fsSL https://opencode.ai/install | bash; providers in ~/.config/opencode/opencode.json use "npm": "@ai-sdk/openai-compatible" + per-model "tools": true
Fallback local CLI Crush (Charmbracelet)SWAP from openclaude CC-style agentic CLI. openclaude dropped: High-sev CVE GHSA-m6rx-7pvw-2f73 + derived from leaked Claude Code source. brew install charmbracelet/tap/crush; provider "type": "openai-compat", base_url …/v1, api_key "not-required"
All-in-one UI Open WebUI :8080 Image + audio + voice + RAG + web-search — wired to both Ollama and the MLX backend DATA_DIR=~/.open-webui uvx --python 3.11 open-webui@latest serve --port 8080; OPENAI_API_BASE_URLS="…11434/v1;…1234/v1" (semicolon-sep)
TTS mlx-audio :8001 (Kokoro fast / Qwen3-TTS) MLX-native, OpenAI-compatible /v1/audio/speech uv tool install --force mlx-audio --prerelease=allow; mlx_audio.server --port 8001 (default is 8000 → always pass --port)
STT parakeet-mlxSWAP primary 3–10× faster than whisper on Apple Silicon. mlx-whisper = multilingual fallback; whisper-cpp = deep fallback. uv tool install parakeet-mlx -U (needs ffmpeg); parakeet-mlx clip.wav --output-format txt
Vision LLM Qwen3-VL-30B-A3B-Instruct-8bit via vllm-mlx serve :8000 OpenAI-compat vision (~38 GB 8-bit) vllm-mlx serve mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit --port 8000 (NOT python -m vllm_mlx.server)
OCR ocrmac (Apple Vision) Native macOS OCR via Python wrapper pip install ocrmac (works on Python 3.13)
Text→image Draw Things :7859 + ComfyUI :8002 Metal FLUX/SD3.5; DT gRPC for automation, Comfy node graphs enable Server-Offload in DT; :8002/prompt for Comfy REST

Important

Port map (no collisions — set --port explicitly on everything): Ollama 11434 · MLX coder (vllm-mlx) 1234 · vllm-mlx vision 8000 · mlx-audio TTS 8001 · Open WebUI 8080 · ComfyUI 8002 (desktop cask; CLI uses 8188) · DT shim 7861 · Draw Things 7860 · SearXNG 8888

Note

As-deployed (2026-05-29): :1234 now serves the heavy Eldadalbajob/Huihui-Qwen3-Coder-Next-abliterated-mlx-4Bit (44.9 GB, served as qwen3-coder-mlx, mmap-loaded). The small mlx-community/Josiefied-Qwen3-1.7B-abliterated-v1-4bit (~1 GB; ~354 decode / ~7,600 prefill tok/s) is the fast fallback. coder-up.sh downloads-then-serves whichever REPO= is set; coder-swap.sh auto-swaps :1234 to the heavy model once its download completes. Persistent chains in ~/.local-ai/ (coder-up.sh, coder-swap.sh, bench-1234.sh, vision-up.sh); logs in ~/.local-ai/logs/. NOTE: launch mlx_audio.server with --log-dir (it crashes creating ./logs if CWD is read-only, e.g. under a LaunchAgent).

Architecture

                     ┌─────────────────────────────────────────────────────┐
                     │  M5 Max 128 GB — iogpu.wired_limit_mb=122880        │
                     │  High Power Mode ON (AC) · MLX 0.31.2 (M5 NAX)      │
                     ├─────────────────────────────────────────────────────┤
   CLOUD LANE   →    │  `claude`  ──→ Anthropic (Claude Max, UNTOUCHED)    │
                     │  ↑ ANTHROPIC_BASE_URL must remain UNSET             │
                     │                                                     │
   LOCAL CLIs   →    │  `opencode` (primary) / `crush` (fallback)          │
                     │     ├─→ MLX perf backend  vllm-mlx :1234/v1  (fast) │
                     │     └─→ Ollama            :11434/v1  (always-warm)  │
                     │                                                     │
   ALL-IN-ONE   →    │  Open WebUI :8080 ──→ BOTH :11434/v1 + :1234/v1     │
                     │   ├─ image gen ──→ Draw Things gRPC :7859           │
                     │   ├─ image gen alt ──→ ComfyUI :8002/prompt         │
                     │   ├─ TTS  ──→ mlx_audio.server :8001 (Kokoro/Qwen3) │
                     │   ├─ STT  ──→ parakeet-mlx (mlx-whisper fallback)   │
                     │   ├─ vision ──→ vllm-mlx :8000 (Qwen3-VL-30B MLX)   │
                     │   ├─ OCR  ──→ ocrmac (Apple Vision Python wrapper)  │
                     │   └─ search ──→ Brave / Tavily / SearXNG / DDG      │
                     └─────────────────────────────────────────────────────┘

Install order

Note

This machine has only Python 3.13. mlx-openai-server needs <3.13 → use vllm-mlx (works on 3.13) as the MLX backend. uv venvs have no pip — install with uv pip install --python <venv>/bin/python … and use the venv's hf/vllm-mlx binaries directly.

# 1. MLX perf backend (vllm-mlx OpenAI server on :1234) — the M5 4× prefill lane
uv venv ~/.venvs/vllm-mlx --python 3.13
uv pip install --python ~/.venvs/vllm-mlx/bin/python vllm-mlx hf_xet
~/.venvs/vllm-mlx/bin/python -c "import mlx.core as mx; print(mx.__version__)"   # must be ≥0.30
# Pre-download the abliterated MLX coder OUTSIDE vllm-mlx's 300s-timeout wrapper, then serve:
#   (unauthenticated HF Xet stalls → force classic HTTPS; or set HF_TOKEN for full speed)
HF_HUB_DISABLE_XET=1 ~/.venvs/vllm-mlx/bin/hf download Eldadalbajob/Huihui-Qwen3-Coder-Next-abliterated-mlx-4Bit
nohup ~/.venvs/vllm-mlx/bin/vllm-mlx serve Eldadalbajob/Huihui-Qwen3-Coder-Next-abliterated-mlx-4Bit \
  --served-model-name qwen3-coder-mlx --port 1234 --continuous-batching --enable-prefix-cache \
  > ~/.local-ai/logs/vllm-mlx-1234.log 2>&1 &
curl -s http://localhost:1234/v1/models   # confirm served id = qwen3-coder-mlx

# 2. Primary local CLI — opencode (org renamed sst → anomalyco)
curl -fsSL https://opencode.ai/install | bash      # → ~/.opencode/bin/opencode (added to PATH)
mkdir -p ~/.config/opencode
cat > ~/.config/opencode/opencode.json <<'EOF'
{
  "$schema": "https://opencode.ai/config.json",
  "provider": {
    "ollama": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "Ollama (local)",
      "options": { "baseURL": "http://localhost:11434/v1" },
      "models": {
        "huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0": { "name": "Qwen3 Coder abliterated 30B", "tools": true },
        "huihui_ai/qwen3-abliterated:30b": { "name": "Qwen3 abliterated 30B", "tools": true }
      }
    },
    "mlx": {
      "npm": "@ai-sdk/openai-compatible",
      "name": "MLX (vllm-mlx :1234)",
      "options": { "baseURL": "http://localhost:1234/v1", "apiKey": "not-required" },
      "models": { "qwen3-coder-mlx": { "name": "Qwen3 Coder Next abliterated MLX", "tools": true } }
    }
  }
}
EOF
opencode --version
# NOTE: the Ollama abliterated *coder* GGUF advertises only `completion` (no tool template),
# so opencode's agent/tool mode can't drive it — use the MLX coder (tools work) or qwen3-abliterated:30b.

# 3. Fallback local CLI — Crush (SWAP from openclaude: CVE GHSA-m6rx-7pvw-2f73 + leaked source)
brew install charmbracelet/tap/crush
mkdir -p ~/.config/crush
cat > ~/.config/crush/crush.json <<'EOF'
{
  "$schema": "https://charm.land/crush.json",
  "providers": {
    "mlx":    { "name": "MLX (local)",    "type": "openai-compat", "base_url": "http://localhost:1234/v1",  "api_key": "not-required",
                "models": [ { "name": "Qwen3 Coder MLX", "id": "qwen3-coder-mlx", "context_window": 32768, "default_max_tokens": 8192 } ] },
    "ollama": { "name": "Ollama (local)", "type": "openai-compat", "base_url": "http://localhost:11434/v1", "api_key": "not-required",
                "models": [ { "name": "Qwen3 Coder abliterated 30B", "id": "huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0", "context_window": 32768, "default_max_tokens": 8192 } ] }
  }
}
EOF

# 4. All-in-one UI (wired to BOTH backends; first boot pulls ~3.4G deps + RAG embedder)
nohup env DATA_DIR=~/.open-webui \
  OPENAI_API_BASE_URLS="http://localhost:11434/v1;http://localhost:1234/v1" \
  OPENAI_API_KEYS="ollama;not-required" \
  ENABLE_OLLAMA_API=true OLLAMA_BASE_URL=http://localhost:11434 \
  uvx --python 3.11 open-webui@latest serve --port 8080 \
  > ~/.local-ai/logs/open-webui-8080.log 2>&1 &
curl -s http://localhost:8080/health      # {"status":true}

# 5. Multimodal stack (Draw Things app already covers text→image)
uv tool install --force mlx-audio --prerelease=allow     # TTS
uv tool install parakeet-mlx -U                          # STT (needs ffmpeg)
uv venv ~/.venvs/ocrmac --python 3.13 && uv pip install --python ~/.venvs/ocrmac/bin/python ocrmac
uv venv ~/.venvs/vllm-mlx-vision --python 3.13 && uv pip install --python ~/.venvs/vllm-mlx-vision/bin/python vllm-mlx hf_xet

# 6. Start the multimodal background services (explicit --port — mlx-audio & vllm-mlx both default to 8000!)
nohup mlx_audio.server --port 8001 > ~/.local-ai/logs/mlx-audio-8001.log 2>&1 &     # TTS :8001
nohup env HF_HUB_DISABLE_XET=1 ~/.venvs/vllm-mlx-vision/bin/vllm-mlx serve \
  mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit --port 8000 \
  > ~/.local-ai/logs/vllm-mlx-vision-8000.log 2>&1 &                                 # vision :8000 (~38 GB)
# Draw Things app: enable "Server-Offload" in Preferences (gRPC :7859)

Smoke test the local lane

# claude stays on cloud — guardrail check
echo "$ANTHROPIC_BASE_URL"           # MUST print nothing
claude --version                     # 2.x, calls Anthropic on first use

# Local coding CLIs hit a LOCAL model (selector syntax: provider/model)
opencode run "list 3 Solidity reentrancy patterns" -m mlx/qwen3-coder-mlx          # MLX backend (tools work)
opencode run "list 3 Solidity reentrancy patterns" -m ollama/huihui_ai/qwen3-abliterated:30b

# Open WebUI up (wired to both :11434 and :1234)
curl -s http://localhost:8080/health || open http://localhost:8080

# MLX coder backend
curl -s http://localhost:1234/v1/models
curl -s http://localhost:1234/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"qwen3-coder-mlx","messages":[{"role":"user","content":"reverse a string in python"}],"max_tokens":80}'

# Vision (Qwen3-VL via vllm-mlx :8000) — base64-encode a local image into the data URL
curl -s http://localhost:8000/v1/chat/completions -H 'Content-Type: application/json' \
  -d '{"model":"mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit","messages":[{"role":"user","content":[{"type":"text","text":"What is this? One word."},{"type":"image_url","image_url":{"url":"data:image/jpeg;base64,'"$(base64 -i /tmp/test.jpg)"'"}}]}],"max_tokens":20}'

# TTS via mlx-audio (one-shot flag is --output_path, not --output)
mlx_audio.tts.generate --model mlx-community/Kokoro-82M-bf16 --text "hello from the M5" --voice af_heart --lang_code a --output_path /tmp/tts_out
afplay /tmp/tts_out.wav

# STT round-trip via parakeet-mlx
parakeet-mlx /tmp/tts_out.wav --output-format txt

# OCR
~/.venvs/ocrmac/bin/python -c "from ocrmac import ocrmac; print(ocrmac.OCR('/tmp/shot.png').recognize()[:3])"

# MAX-GPU proof: run a big MLX generation and watch the GPU
sudo powermetrics --samplers gpu_power -n 3 -i 1000     # GPU active residency + power
pmset -g | grep powermode                               # expect powermode 2 on AC

Important

The two lanes are independent. When Adam is online, both work in parallel — use claude for hard problems where you want Anthropic's best, opencode/Open WebUI for privacy, offline work, or burst-rate-limited tasks. Models stay warm in Ollama because both local tools share the same Ollama backend.

Note

Why not just use Open WebUI for everything? It's a web UI, not an agentic terminal coding tool. It can't drive multi-turn file edits the way opencode/claude do. Pair them: opencode for code-and-edit loops, Open WebUI for the everything-else surface (image, voice, search, RAG).

M5 max-utilization tuning

To actually exploit the M5 Max (40-core GPU, per-core Neural Accelerators, ~600 GB/s):

# High Power Mode (AC only) — sustains GPU clocks under long MLX loads
sudo pmset -c powermode 2          # set for AC profile; engages when plugged in
pmset -g | grep powermode          # live value shows 2 on AC

# MLX must be ≥0.30 for the M5 Neural-Accelerator path (up to 4× prefill vs M4; needs macOS ≥26.2)
python -c "import mlx.core as mx; print(mx.__version__)"   # have 0.31.2 ✓

# Ollama: keep models warm + raise context (tiny default silently truncates → broken tool calls)
#   set in ~/Library/LaunchAgents/homebrew.mxcl.ollama.plist EnvironmentVariables:
#   OLLAMA_KEEP_ALIVE=-1 · OLLAMA_CONTEXT_LENGTH=16384 (alongside OLLAMA_FLASH_ATTENTION=1, OLLAMA_KV_CACHE_TYPE=q8_0)
launchctl bootout gui/$(id -u)/homebrew.mxcl.ollama; launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/homebrew.mxcl.ollama.plist
  • MLX_METAL_FAST_SYNCH is bug-prone (kernel-load failures / deadlocks) — do NOT blanket-enable. It's gated behind the opt-in mlx_max shell function (with caffeinate + sleep-off); call it only when deliberately benchmarking.
  • GPU cap stays iogpu.wired_limit_mb=122880 (120 GB). May lower to 118784 (116 GB) if running everything at once and memory pressure goes red (avoids the IOGPU kernel panic). Never 124000+.
  • Decode is bandwidth-bound (MoE models win), prefill is compute-bound (where the M5 4× shows up) — prefer MLX MoE 4-bit for coding.

Verified gotchas (Phase 17)

  • HF Xet stalls on unauthenticated pulls. Big MLX repos are Xet-backed; without an HF_TOKEN the transfer wedges (repeated xet-read-token, ~0 bytes). Fix: export HF_HUB_DISABLE_XET=1 (classic HTTPS/LFS) or set an HF_TOKEN. hf_xet must be installed in the venv either way.
  • vllm-mlx wraps downloads in a 300s-per-attempt timeout → multi-GB shards never finish over a slow link. Pre-download with hf download <repo> (no timeout, resumable), then vllm-mlx serve (loads from cache).
  • Python 3.13 only on this boxmlx-openai-server (needs <3.13) is out; use vllm-mlx. uv venvs have no pip (use uv pip install --python …).
  • mlx-audio & vllm-mlx both default to :8000 → always pass --port (TTS=8001, vision=8000).
  • mlx-audio one-shot flag is --output_path, not --output; Kokoro repo id mlx-community/Kokoro-82M-bf16.
  • opencode org renamed sstanomalyco; brew tap is anomalyco/tap/opencode (curl installer + ~/.config/opencode/opencode.json path unchanged).
  • openclaude → Crush: openclaude has High-sev CVE GHSA-m6rx-7pvw-2f73 and is derived from leaked Claude Code source.
  • Ollama abliterated coder GGUF lacks a tool template (advertises only completion) → opencode/Crush agent mode can't drive it; use the MLX coder (tools work) or qwen3-abliterated:30b.

Nexus — the control layer (2026-05-29)

"Nexus" wraps the whole local-AI stack in a one-click macOS experience. All control scripts live in ~/.local-ai/; logs in ~/.local-ai/logs/.

App (single, in /Applications):

  • Nexus.appmenu-bar app (Swift/NSStatusItem, LSUIElement, bundle id local.adam.nexus, v2.0). Menu: Open Web UI / Open SearXNG / Start All & Open Web UI / Start All / Stop All / Restart All / Free RAM (unload models) / View Logs / Quit. Source ~/.local-ai/nexusbar/App.swift (swiftc -O -parse-as-library -target arm64-apple-macos26.0 -framework AppKit, ad-hoc codesigned). Consolidated 2026-05-30 (former Nexus.app start launcher + Nexus Stop.app + NexusBar.app merged into this).
  • Auto-start at login: LaunchAgents ~/Library/LaunchAgents/{local.adam.nexus,com.adam.nexus}.plist. local.adam.nexus opens the menu-bar app; com.adam.nexus runs start-all.sh to boot the services. Load: launchctl bootstrap gui/$(id -u) <plist> (disable: launchctl bootout gui/$(id -u)/<label>). NOTE: in zsh ! cmd1 && cmd2 negates cmd1 → the && short-circuits; run each bootstrap on its own line.

Scripts: start-all.sh (idempotent: Ollama, MLX coder :1234, MLX vision :8000, mlx-audio :8001, OrbStack+SearXNG :8888, DT shim :7861, Open WebUI :8080, then opens browser; --no-open skips that), stop-all.sh, webui-up.sh, coder-up.sh, vision-up.sh, dt-a1111-shim.py, vision-mcp.py, owui-token.sh.

Port map: Ollama 11434 · MLX coder 1234 · MLX vision 8000 · mlx-audio TTS 8001 · DT A1111 shim 7861 · Open WebUI 8080 · SearXNG 8888 · Draw Things gRPC/HTTP 7859/7860.

Open WebUI features (all local, env in webui-up.sh; some configs persist to webui.db so env can be overridden — verify via API):

  • Web search ON by default: SearXNG (docker, :8888) with BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL=true. Set the per-user setting webSearch: "always" (Settings → Interface → "Always on Web Search", or DB user.settings) so every chat auto-searches — the frontend reads $settings.webSearch === 'always'.
  • Audio: TTS = mlx-audio Kokoro (af_heart); STT = Open WebUI's local faster-whisper (mlx-audio's parakeet route is broken). mlx-audio must run with HF_HUB_OFFLINE=1.
  • Image gen: Open WebUI → ~/.local-ai/dt-a1111-shim.py (stdlib proxy :7861) → Draw Things :7860. DT's API omits sd_model_checkpoint (→ OWUI KeyError 400) and 404s sd-models; the shim injects it + ensures txt2img responses carry info. AUTOMATIC1111_BASE_URL=http://localhost:7861. One-time: Draw Things → Advanced → API Server ON, HTTP, port 7860; keep DT open.
  • Connected models: MLX coder (qwen3-coder-mlx), MLX vision (Qwen3-VL :8000), all Ollama abliterated models.

Local Claude-Code-like TUI = opencode (~/.config/opencode/opencode.json): wired to all local coders + a vision MCP server (~/.local-ai/vision-mcp.py, run from ~/.venvs/mcp-vision) that calls Qwen3-VL :8000 — so opencode reads images offline (its native image-attach to custom providers is broken, anomalyco/opencode#20802). Skills in ~/.config/opencode/skills/<name>/SKILL.md, subagents in ~/.config/opencode/agent/<name>.md. crush = secondary TUI (best native image attach). In-editor offline AI = Continue.dev (VS Code, ~/.continue/config.yaml): autocomplete via Ollama abliterated coder, chat/edit via the MLX coder on :1234, embeddings via nomic-embed-text. Cursor was evaluated and rejected for this stack (cloud-routed model proxy + Chat-only custom model honoring).

M5 memory: Ollama OLLAMA_KEEP_ALIVE=30m + OLLAMA_MAX_LOADED_MODELS=1 (was -1, which pinned the 122B+30B = ~106 GB forever and forced swap). Don't run the 122B and the 38 GB Qwen3-VL together.


Step 23 — Post-install manual checklist (not scripted)

setup.sh installs every formula and cask, but it CANNOT grant TCC permissions, sign you in, enter licenses, run first-launch wizards, or install editor/launcher extensions. Work through this once on a fresh Mac, after setup.sh finishes. Items marked (scriptable) can be automated and may move into setup.sh later.

Already covered elsewhere — do NOT repeat here: Apple ID / iCloud-off / FileVault (top of doc), VS Code Settings Sync (Step 8), Claude Code OAuth (Step 9), 1Password OAuth + Brave extensions (Step 10b), Hazel license (Step 11), Continue.dev wiring (Step 22.5). The Nexus architecture (ports, scripts, Open WebUI features) is in the Nexus section above; subsection (E) is only the one-time human actions it assumes.

(A) macOS permissions to grant

TCC (privacy) grants cannot be scripted. Open System Settings → Privacy & Security, pick the category, and add the app with + from /Applications if it isn't listed. Apps that need a permission do nothing until you grant it.

App Permission Where (System Settings → Privacy & Security → …)
AltTab Accessibility (required — focus/switch) Accessibility
Raycast Accessibility Accessibility
VoiceInk Accessibility (paste back transcript) Accessibility
CleanShot X Accessibility Accessibility
Zoom Accessibility (remote control only) Accessibility
CleanShot X Screen Recording (required) Screen & System Audio Recording
Zoom Screen Recording (share screen) Screen & System Audio Recording
AltTab Screen Recording (window previews) Screen & System Audio Recording
Raycast Input Monitoring (global hotkey) Input Monitoring
VoiceInk Input Monitoring (push-to-talk hotkey) Input Monitoring
VoiceInk Microphone (required — dictation) Microphone
Zoom Microphone + Camera Microphone / Camera
WhatsApp / Telegram Microphone + Camera (calls) Microphone / Camera
Hazel Full Disk Access (required) — then Quit & Reopen Hazel Full Disk Access
Hazel → Finder Automation (move/tag files) — approve the prompt Automation
Ghostty Files & Folders — approve per-folder prompts on first access Files & Folders

(B) Per-app first-run config / sign-ins / licenses

Sign-ins

  • Tailscale — app reports "Logged out": menu-bar icon → Log in → join your tailnet (also flip Run at login in (D)).
  • Telegram — phone number + 2FA. WhatsApp — Settings → Linked Devices → scan QR. Zoom — Google SSO (then grant perms in (A)). Postman — sign in for sync, or use offline/Scratchpad mode and import collections.

Licenses + first-run config

  • CleanShot X — enter license. Disable ALL 5 native screenshot shortcuts (System Settings → Keyboard → Shortcuts → Screenshots → uncheck the 5 entries). Then in CleanShot Settings → Shortcuts, bind: ⌘⇧3 → Capture Fullscreen, ⌘⇧4 → Capture Area, ⌘⇧5 → Record Video, ⌘⇧T → Capture Text (OCR), ⌘⇧G → Record GIF, ⌘⇧8 → Capture Previous Area. Save to ~/Pictures/Screenshots; enable OCR + scrolling capture; turn ON displayRecordingTime, doNotDisturbWhileRecording. CleanShot fully replaces native screenshots + recording.
  • VoiceInkAI Models → Local → Import Local Model = the already-present ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin (do NOT re-download) → Set as Default; set the recording hotkey; optional Enhancement → provider Ollama (auto-connects localhost:11434, already running); enter license.
  • Ice — set the menu-bar divider; choose Hidden vs Always-Hidden icons. (Tahoe note: Ice 0.11.12 is partly broken on macOS 26; if hiding misbehaves use a 0.11.13-dev beta.)
  • Stats — pick modules (CPU / GPU / RAM / Network / Sensors). The Ghostty config in Step 21 references a stats block — verify it matches.
  • Cold Turkey BlockerSKIP. Removed 2026-05-30 due to persistence bug on macOS 26.5 Tahoe (imports never survive restart, even on fresh install — confirmed via SQLite forensics). Replacement TBD.
  • LM Studio — no models: download a GGUF in-app, or just rely on Ollama (Step 14).
  • ComfyUI — first launch builds the support-dir tree; drop a checkpoint (SDXL / FLUX .safetensors) into models/checkpoints; for the local-AI lane enable listen on :8002.

(C) Extensions & plugins

VS Code — sign in (top-right) for Settings Sync (Microsoft/GitHub OAuth). Fresh install has zero extensions. Install via CLI (scriptable)code is auto-linked at /opt/homebrew/bin/code:

# Solidity / web3
code --install-extension NomicFoundation.hardhat-solidity   # official Hardhat LSP, reads foundry.toml
code --install-extension tintinweb.solidity-visual-auditor  # works on VS Code (MS Marketplace)
code --install-extension tintinweb.vscode-solidity-flattener
# web / TS / React
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension bradlc.vscode-tailwindcss
code --install-extension prisma.prisma
code --install-extension christian-kohler.path-intellisense
code --install-extension usernamehw.errorlens
code --install-extension eamodio.gitlens
code --install-extension yoavbls.pretty-ts-errors
code --install-extension mikestead.dotenv
# general
code --install-extension streetsidesoftware.code-spell-checker
code --install-extension gruntfuggly.todo-tree
code --install-extension editorconfig.editorconfig
# fully-offline in-editor AI → Continue.dev wired to local stack
code --install-extension Continue.continue   # config: ~/.continue/config.yaml

Both tintinweb Solidity extensions install cleanly on VS Code (MS Marketplace). solidity-visual-auditor pulls in tintinweb.vscode-ethover, tintinweb.graphviz-interactive-preview, tintinweb.vscode-inline-bookmarks, tintinweb.vscode-solidity-language, and jebbs.plantuml as dependencies — ~21 total ext after install. Continue.dev's config.yaml is pre-wired to the MLX coder (port 1234), Ollama abliterated coders (11434), and nomic-embed-text — see Step 22.5.

Then set Foundry formatting in VS Code settings.json (~/Library/Application Support/Code/User/settings.json) and verify with code --list-extensions:

{
  "[solidity]": { "editor.defaultFormatter": "NomicFoundation.hardhat-solidity", "editor.formatOnSave": true },
  "solidity.formatter": "forge",
  "solidity.packageDefaultDependenciesContractsDirectory": "src",
  "solidity.packageDefaultDependenciesDirectory": "lib"
}

Raycast

  • Enable built-ins (no Store install): Clipboard History (30-day), Window Management, Snippets, Calculator, File Search, Emoji Picker.
  • Store extensions (⌘↵ to install): GitHub, Brew, Speedtest, Color Picker, Coffee, Kill Process, Visual Studio Code recent projects, DevDocs, 1Password.
  • Finish replacing Spotlight: System Settings → Keyboard → Keyboard Shortcuts → Spotlight → uncheck ⌘Space. Confirm Raycast is on ⌥Space.

Obsidian — no vault exists. Create one on local disk (e.g. ~/2_KNOWLEDGE/vault; iCloud Drive is OFF). Settings → Community plugins → disable Restricted mode → Browse → install Dataview, Templater, QuickAdd, Tasks, Obsidian Git (optional: Advanced Tables, Excalidraw, Linter, Calendar, Homepage). Keep under ~30 plugins. Pairs with the Web Clipper extension in Step 10b.

Chrome — Chrome is the backup browser; do NOT mirror Brave's Step 10b list. Only: Google profile sign-in, 1Password (from the desktop app's Browsers pane), optional React Developer Tools. Ad/tracker blocking + wallet stay Brave's job.

(D) Login items & menu-bar auto-start

Three mechanisms: (1) classic Login Item (System Settings → General → Login Items & Extensions, inspect via osascript -e 'tell application "System Events" to get the name of every login item'); (2) in-app SMAppService toggle (inspect via sudo sfltool dumpbtm); (3) a LaunchAgent plist in ~/Library/LaunchAgents with RunAtLoad.

Already auto-start (no clicks needed): Raycast (classic login item), AltTab (LaunchAgent + in-app toggle), CleanShot X (SMAppService), Ollama (brew LaunchAgent homebrew.mxcl.ollama.plist).

Need a one-time GUI toggle (no CLI):

  • Ice — right-click menu bar → Ice Settings → General → Launch at login ON.
  • Stats — Settings → Start at login ON.
  • VoiceInk — Settings → Launch at login ON (also grant Microphone + Accessibility/Input Monitoring per (A)).
  • Tailscale — menu → Settings → Run Tailscale at login ON (it's running now only because launched manually).

Touch ID for sudo (scriptable): sudo ships /etc/pam.d/sudo_local.template but no active sudo_local. Enable Touch-ID-backed sudo:

sudo sed 's/^#auth/auth/' /etc/pam.d/sudo_local.template | sudo tee /etc/pam.d/sudo_local
# optional: add `auth sufficient pam_watchid.so` for Apple Watch unlock

(E) Local-AI (Nexus) manual steps

One-time human actions the Nexus lane assumes (architecture is in the Nexus section above).

  1. Install opencode — MISSING from setup.sh/Brewfile (which opencode = not found): curl -fsSL https://opencode.ai/install | bash (or brew install anomalyco/tap/opencode). Config is already documented at ~/.config/opencode/opencode.json; launch with opencode. (scriptable)
  2. Draw Things API Server (required for image gen, off by default, not scriptable): Draw Things → Settings → Advanced → API Server ON, HTTP, port 7860; keep DT running. Verify curl -s http://localhost:7860/sdapi/v1/sd-models. ⚠️ Known bug (upstream drawthingsai/draw-things-community#59, open since 2026-03): the toggle does NOT persist between app launches — every relaunch reverts API Server to OFF. AppleScript UI auto-toggle won't help (DT is pure SwiftUI with zero accessibility labels). Workaround: accept the manual re-toggle each session, or use Open WebUI's native ComfyUI integration via :8002 and bypass DT entirely.
  3. Open WebUI first-run (browser only): open http://localhost:8080 — the first account becomes admin. Then Settings → Interface → Always on Web Search ON (auto-queries SearXNG :8888). Verify curl -s http://localhost:8080/health.
  4. Start now: Nexus.app menu → Start All & Open Web UI, or ~/.local-ai/start-all.sh (--no-open skips the browser; stop-all.sh frees RAM). Auto-start at login (scriptable) — run each on its own line (in zsh, ! a && b short-circuits):
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.adam.nexus.plist
    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.adam.nexus.plist
    Ollama auto-starts via its own brew LaunchAgent.
  5. One-time grants: the DT API toggle (E.2), Open WebUI admin signup (E.3), and OrbStack's privileged-helper prompt on first launch (approve once). No cloud logins; ANTHROPIC_BASE_URL stays unset (Step 22.5 guardrail).

Cheat sheet — keyboard shortcuts

Shortcut Action
⌥+Space Raycast launcher
⌘+⇧+3 CleanShot fullscreen
⌘+⇧+4 CleanShot region
⌘+⇧+5 CleanShot record video
⌘+⇧+T CleanShot capture text (OCR)
⌘+⇧+G CleanShot record GIF
⌘+⇧+8 CleanShot capture previous area
Cmd+Shift+Ctrl+4 Region → clipboard (for Claude image paste)
⌥+Tab AltTab window switcher with previews
Ctrl+R Atuin fuzzy history search
Ctrl+T fzf file finder
Alt+C fzf cd subdir
z foo zoxide jump to most-visited "foo" dir
cc Claude Code (Ghostty paste-safe)
code . Open current folder in VS Code
lg Lazygit TUI
mlx_max / mlx_revert Toggle MLX max-power mode
`⌘+`` Ghostty Quake-style Quick Terminal (any app)
⌘+⇧+P Ghostty Command Palette

Cost summary

  • Disk — ~30 GB tools + ~80 GB models = ~110 GB after full install
  • Active install time — ~90 min (downloads = overnight)
  • Recurring monthly — Claude Pro $20 + Raycast Pro $8 = ~$28/mo (VS Code free, Continue.dev free)
  • One-time — CleanShot $30 (+ optionally Hazel $42 if you want event-driven GUI rules instead of the launchd script in §21.9/§21.11). Cold Turkey removed 2026-05-30 (Tahoe persistence bug, see Step 11).

That's it. Every command above is annotated with what it does to your system. ~30 brew formulae + ~25 casks + 7 Python tools + 4 Ollama models + 8 config files. Production-ready M5 Max.

— Adam (fanout-verified, mnemosyne-checked, fully explained, 2026-05-27)

Companion gist — Piyush base setup : https://gist.github.com/Aboudjem/da6b37f5d58a9e8b1b238e11a860d3d3

#!/bin/zsh
# Adam's ADHD-friendly file cleanup — mirrors ~/2_KNOWLEDGE/hazel-adhd-rules.md (13 rules).
# Scheduled via ~/Library/LaunchAgents/com.adam.adhd-cleanup.plist (every 10 min).
# Idempotent and safe to re-run. mv -n prevents overwrites. Logs to ~/.local-ai/logs/adhd-cleanup.log.
#
# Manual: zsh ~/.local-bin/adhd-cleanup.sh
# Dry-run: DRY_RUN=1 zsh ~/.local-bin/adhd-cleanup.sh (prints intended moves, doesn't touch files)
set -u
LOG="$HOME/.local-ai/logs/adhd-cleanup.log"
mkdir -p "$(dirname "$LOG")"
DRY_RUN=${DRY_RUN:-0}
log() { print -P "[%D{%H:%M:%S}] $1" >> "$LOG"; }
moved=0; trashed=0
do_mv() {
local src="$1" dest="$2"
[[ -e "$src" ]] || return
mkdir -p "$dest"
if (( DRY_RUN )); then
log "DRY mv: $src → $dest/"
else
if mv -n "$src" "$dest/" 2>>"$LOG"; then
((moved++)); log "mv: $src → $dest/"
fi
fi
}
do_trash() {
local src="$1"
[[ -e "$src" ]] || return
if (( DRY_RUN )); then
log "DRY trash: $src"
else
if mv -n "$src" "$HOME/.Trash/" 2>>"$LOG"; then
((trashed++)); log "trash: $src"
fi
fi
}
archive_dated() {
local src="$1" dest_root="$2"
[[ -e "$src" ]] || return
local mon; mon=$(date -r "$src" +%Y-%m 2>/dev/null) || return
do_mv "$src" "$dest_root/$mon"
}
do_delete_permanent() {
local src="$1"
[[ -e "$src" ]] || return
if (( DRY_RUN )); then
log "DRY rm: $src"
else
if rm -rf -- "$src" 2>>"$LOG"; then
((trashed++)); log "rm: $src"
fi
fi
}
log "=== start (DRY_RUN=$DRY_RUN) ==="
# Excluded names (case-sensitive on APFS by default)
EXCL=( ! -name ".DS_Store" ! -name ".localized" )
#####################
# ~/Downloads rules
#####################
# 1. DMGs older than 1 day → Trash
while IFS= read -r f; do do_trash "$f"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f -iname "*.dmg" -mtime +1 2>/dev/null
)
# 2. .pkg installers older than 3 days → Trash
while IFS= read -r f; do do_trash "$f"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f -iname "*.pkg" -mtime +3 2>/dev/null
)
# 3. Stale archives older than 14 days → Trash
while IFS= read -r f; do do_trash "$f"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f \
\( -iname "*.zip" -o -iname "*.tar" -o -iname "*.tar.gz" -o -iname "*.tgz" -o -iname "*.gz" -o -iname "*.7z" -o -iname "*.rar" \) \
-mtime +14 2>/dev/null
)
# 4. Screenshots → ~/Pictures/Screenshots
while IFS= read -r f; do do_mv "$f" "$HOME/Pictures/Screenshots"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f \
\( -iname "Screen Shot*" -o -iname "Screenshot*" -o -iname "CleanShot*" \) 2>/dev/null
)
# 5. invoice/facture PDFs → ~/Documents/3_admin/factures
while IFS= read -r f; do do_mv "$f" "$HOME/Documents/3_admin/factures"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f -iname "*.pdf" \
\( -iname "*invoice*" -o -iname "*facture*" \) 2>/dev/null
)
# 6. receipt PDFs → ~/Documents/3_admin/receipts
while IFS= read -r f; do do_mv "$f" "$HOME/Documents/3_admin/receipts"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f -iname "*.pdf" \
\( -iname "*receipt*" -o -iname "*recu*" -o -iname "*reçu*" \) 2>/dev/null
)
# 7. audit*.pdf → ~/2_KNOWLEDGE/audits/incoming
while IFS= read -r f; do do_mv "$f" "$HOME/2_KNOWLEDGE/audits/incoming"; done < <(
find -H "$HOME/Downloads" -maxdepth 1 -type f -iname "audit*.pdf" 2>/dev/null
)
# 8. Stale-everything sweep — anything older than 30 days → ~/Downloads/_archive/YYYY-MM
while IFS= read -r f; do archive_dated "$f" "$HOME/Downloads/_archive"; done < <(
find -H "$HOME/Downloads" -mindepth 1 -maxdepth 1 -mtime +30 \
! -name "_archive" "${EXCL[@]}" 2>/dev/null
)
#####################
# ~/Desktop rules
#####################
# 9. Desktop screenshot rescue
while IFS= read -r f; do do_mv "$f" "$HOME/Pictures/Screenshots"; done < <(
find -H "$HOME/Desktop" -maxdepth 1 -type f \
\( -iname "Screen Shot*" -o -iname "Screenshot*" -o -iname "CleanShot*" \) 2>/dev/null
)
# 10. Daily Desktop sweep — anything older than 1 day → ~/Desktop/_inbox
while IFS= read -r f; do do_mv "$f" "$HOME/Desktop/_inbox"; done < <(
find -H "$HOME/Desktop" -mindepth 1 -maxdepth 1 -mtime +1 \
! -name "_inbox" "${EXCL[@]}" 2>/dev/null
)
#####################
# ~/Pictures/Screenshots rules
#####################
# 11. Screenshot archive older than 14 days → archive/YYYY-MM
while IFS= read -r f; do archive_dated "$f" "$HOME/Pictures/Screenshots/archive"; done < <(
find -H "$HOME/Pictures/Screenshots" -mindepth 1 -maxdepth 1 -mtime +14 \
! -name "archive" "${EXCL[@]}" 2>/dev/null
)
#####################
# ~/Documents/0_inbox rules
#####################
# 12. 0_inbox older than 14 days → ~/Documents/_archive/YYYY-MM
while IFS= read -r f; do archive_dated "$f" "$HOME/Documents/_archive"; done < <(
find -H "$HOME/Documents/0_inbox" -mindepth 1 -maxdepth 1 -mtime +14 \
"${EXCL[@]}" 2>/dev/null
)
#####################
# ~/.Trash rules
#####################
# 13. Auto-empty Trash items WHOSE INODE CHANGE TIME (=when moved into Trash) is older than 14 days.
# Using -ctime instead of -mtime avoids deleting freshly-trashed apps with old build dates.
while IFS= read -r f; do do_delete_permanent "$f"; done < <(
find -H "$HOME/.Trash" -mindepth 1 -maxdepth 1 -ctime +14 2>/dev/null
)
#####################
# Tier C — content classification (OCR + Ollama)
#####################
# For PDFs / images / text files in ~/Downloads or ~/Desktop that survived the rules above,
# extract text via pdftotext / ocrmac and ask Ollama to classify + name.
# Rate-limit: max CLASSIFY_LIMIT files per cycle (each ~3-10s of Ollama latency).
# Skip-mark via xattr so files we've already tried aren't re-classified every 10 min.
CLASSIFY_LIMIT=${CLASSIFY_LIMIT:-5}
CLASSIFY_MIN_AGE_MIN=2 # ignore files modified in last N minutes (in-progress downloads)
SKIP_XATTR="com.adam.adhd-classify"
classified=0
# Skip classification entirely if Ollama is not responding
if (( ! DRY_RUN )); then
if ! /usr/bin/curl -s --max-time 2 http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
log "classify: ollama down, skipping content tier"
else
for root in "$HOME/Downloads" "$HOME/Desktop"; do
(( classified < CLASSIFY_LIMIT )) || break
while IFS= read -r f; do
(( classified < CLASSIFY_LIMIT )) || break
[[ -e "$f" ]] || continue
# Skip if we already tried and marked
if /usr/bin/xattr -p "$SKIP_XATTR" "$f" >/dev/null 2>&1; then
continue
fi
# Classify
out=$(/opt/homebrew/bin/python3 "$HOME/.local-bin/classify-file.py" "$f" 2>>"$LOG")
cat=$(print -r -- "$out" | /opt/homebrew/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('category','skip'))" 2>/dev/null)
case "$cat" in
invoice|receipt|bank-statement|audit-report|contract|tax-doc|code-or-tech-doc|screenshot-code|screenshot-other|photo)
route=$(print -r -- "$out" | /opt/homebrew/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('route',''))" 2>/dev/null)
name=$(print -r -- "$out" | /opt/homebrew/bin/python3 -c "import sys,json; print(json.load(sys.stdin).get('name',''))" 2>/dev/null)
ext="${f##*.}"
if [[ -n "$route" && -d "$(dirname "$route")" ]]; then
mkdir -p "$route"
target="$route/${name:-$(basename "$f" .${ext})}.${ext}"
# Suffix if exists
n=1
while [[ -e "$target" ]]; do
target="$route/${name:-$(basename "$f" .${ext})}-$n.${ext}"
(( n++ ))
done
if mv -n "$f" "$target" 2>>"$LOG"; then
((moved++)); ((classified++))
log "classify: $f → $target (cat=$cat)"
fi
fi
;;
skip|*)
# Mark so we don't retry every cycle
/usr/bin/xattr -w "$SKIP_XATTR" "skip-$(date +%Y%m%d)" "$f" 2>/dev/null
log "classify-skip: $f ($out)"
;;
esac
done < <(
find -H "$root" -mindepth 1 -maxdepth 1 -type f \
\( -iname "*.pdf" -o -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.png" -o -iname "*.heic" -o -iname "*.txt" -o -iname "*.md" -o -iname "*.csv" \) \
-mmin +$CLASSIFY_MIN_AGE_MIN \
! -name ".*" "${EXCL[@]}" 2>/dev/null
)
done
fi
fi
log "=== end moved=$moved trashed=$trashed classified=$classified ==="
print "moved=$moved trashed=$trashed classified=$classified log=$LOG"
# Brewfile — Adam's M5 Max install (May 2026, fanout-verified)
#
# Usage:
# brew bundle install --file=Brewfile
#
# What this installs:
# ~30 CLI formulae + ~25 GUI casks + 5 external taps
# ~10 GB disk, ~30 min wall-clock
#
# What this does NOT install (run setup.sh after):
# - Foundry (curl installer)
# - Solidity Python tools (uv tool install)
# - Claude Code, Vercel, pnpm (npm install -g)
# - Python MLX libs (uv add in project dir)
# - Ollama model pulls
# - macOS defaults / sysctl / launchd
# - Hazel license, ssh-keygen, OAuth logins (manual)
# ============================================================
# === External taps (community-maintained repos) ===
tap "cyfrin/tap" # aderyn (Solidity static analyzer)
tap "stripe/stripe-cli" # stripe CLI
tap "charmbracelet/tap" # crush — Phase 17 fallback coding TUI
# tap "supabase/tap" # RELOCATED — supabase now in homebrew-core
# tap "mystenlabs/tap" # RELOCATED — sui now in homebrew-core
# ============================================================
# CLI FORMULAE
# ============================================================
# --- Shell foundation ---
brew "antidote" # zsh plugin manager (faster than oh-my-zsh)
brew "starship" # cross-shell prompt
brew "atuin" # encrypted shell history sync
# --- Modern Unix replacements ---
brew "eza" # better ls (git status, colors)
brew "bat" # better cat (syntax highlight)
brew "fd" # better find
brew "ripgrep" # better grep (binary: rg)
brew "fzf" # fuzzy finder (Ctrl+R, Ctrl+T, Alt+C)
brew "zoxide" # smart cd (z foo)
# --- Git + dev TUI ---
brew "lazygit" # git TUI
brew "yazi" # file manager TUI
brew "gh" # GitHub CLI
# --- Data processing ---
brew "jq" # JSON processor
brew "yq" # YAML processor
brew "tree" # directory tree
# --- System monitoring ---
brew "htop" # process viewer
brew "btop" # pretty process viewer
brew "dust" # better du (disk usage tree)
brew "duf" # better df (free space)
# --- Dev utilities ---
brew "direnv" # auto-load .envrc per folder
brew "tldr" # short man pages
brew "gnupg" # provides gpg (commit signing)
brew "mkcert" # local HTTPS certs
# --- Network / API ---
brew "mas" # Mac App Store CLI
brew "xh" # rust port of httpie (faster)
brew "doggo" # modern dig
brew "wget" # download with resume
brew "cloudflared" # cloudflare tunnel
# --- Misc ---
brew "watch" # repeat a command every N seconds
brew "imagemagick" # image processing (resize/convert)
brew "poppler" # pdftotext + pdftoppm — adhd-cleanup Tier C classify pipeline
brew "yt-dlp" # youtube downloader
brew "ncdu" # interactive disk usage
# --- Dev runtimes / managers ---
brew "mise" # polyglot version manager (node/python/rust/go)
brew "uv" # python package + venv manager (Astral)
brew "git" # newer than Xcode CLT git (2.54 vs 2.39)
# --- AI / MLX local stack ---
brew "ollama" # local LLM runtime (drives opencode/Continue.dev — NEVER the `claude` CLI)
brew "whisper-cpp" # speech-to-text via Metal (NEVER use pip whisper)
brew "espeak-ng" # offline TTS (paired with whisper-cpp for voice pipelines)
brew "ffmpeg" # required transitive dep for whisper + Python AI libs
brew "llama.cpp" # GGUF inference
brew "charmbracelet/tap/crush" # Phase 17 fallback coding TUI (replaced openclaude — CVE GHSA-m6rx-7pvw-2f73)
brew "dockutil" # scriptable Dock add/remove (CLI Dock layout helper)
# --- Web3 / Solidity ---
brew "echidna" # Haskell smart-contract fuzzer
brew "solana" # Solana CLI v4 (Agave/Anza build)
brew "aptos" # Aptos Move chain CLI
brew "cyfrin/tap/aderyn" # Cyfrin Rust static analyzer (no core formula yet)
brew "sui" # Sui Move chain CLI (relocated to core, was mystenlabs/tap)
brew "supabase" # Supabase CLI (relocated to core, was supabase/tap)
brew "stripe/stripe-cli/stripe" # Stripe CLI (Stripe-owned tap, no core formula)
# --- Cloud CLIs ---
brew "awscli" # AWS CLI v2 (formula is awscli, not aws-cli)
brew "doctl" # DigitalOcean
# ============================================================
# GUI CASKS
# ============================================================
# --- Terminal + editor ---
cask "ghostty" # GPU terminal (multi-line paste needs TERM=xterm-256color in Claude Code)
cask "visual-studio-code" # editor (uses MS Marketplace; Continue.dev wires local Ollama/MLX)
# --- Browsers ---
cask "brave-browser" # primary — Chromium + native adblock/tracker block + crypto wallet
cask "google-chrome" # backup — universal DevTools reference, sites that detect Brave
cask "firefox" # cross-browser testing (non-Chromium engine)
# cask "zen" # OPTIONAL: Firefox fork with workspaces + vertical tabs (cask=zen NOT zen-browser)
# --- ADHD power stack ---
cask "raycast" # launcher (replaces Spotlight + Alfred + Maccy + Paste)
cask "jordanbaird-ice" # menubar declutter (NOT thaw/bartender)
cask "obsidian" # local-first markdown notes
# --- Cloud CLIs (casks) ---
cask "tailscale-app" # mesh VPN (NOT tailscale)
# --- AI / MLX GUIs ---
cask "lm-studio" # local LLM chat GUI
cask "draw-things" # image gen GUI (SD, FLUX, Z-Image)
cask "comfyui" # node-based image/video workflows
# --- Docker ---
cask "orbstack" # fast Docker for Mac (NOT Docker Desktop)
# --- Fonts ---
cask "font-jetbrains-mono-nerd-font" # required by Ghostty config (setup.sh §21.4)
# --- Productivity ---
cask "cleanshot" # screenshots + screen recording + OCR
# cask "rectangle" # OPTIONAL: window snapping
cask "1password" # password manager
cask "1password-cli" # op CLI for secret injection
cask "postman" # API testing GUI
# --- Communications ---
cask "whatsapp"
cask "telegram"
cask "zoom"
# --- System utilities ---
cask "alt-tab" # window-level cmd-tab with previews
cask "stats" # menubar system monitor
cask "appcleaner" # proper uninstaller
cask "the-unarchiver" # .rar / .7z / .tar.xz
cask "iina" # modern video player
# cask "spotify" # OPTIONAL — Adam uses Apple Music (native). Uncomment if you switch.
cask "voiceink" # MLX dictation (Whisper-Turbo on GPU)
#!/usr/bin/env python3
"""
Classify a file by extracting its text (pdftotext / ocrmac / head) and asking Ollama
to categorize. Prints JSON {category, name, confidence, route, source} to stdout, or
{category:"skip", reason:...} if not classifiable.
Used by ~/.local-bin/adhd-cleanup.sh for content-aware routing of unmatched files.
Usage: classify-file.py <path>
"""
import json
import os
import subprocess
import sys
from pathlib import Path
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
OLLAMA_MODEL = "huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0"
MAX_TEXT_CHARS = 4000
MIN_TEXT_CHARS = 40
CATEGORIES = [
"invoice",
"receipt",
"bank-statement",
"audit-report",
"contract",
"tax-doc",
"code-or-tech-doc",
"screenshot-code",
"screenshot-other",
"photo",
"other",
]
ROUTES = {
"invoice": os.path.expanduser("~/Documents/3_admin/factures"),
"receipt": os.path.expanduser("~/Documents/3_admin/receipts"),
"bank-statement": os.path.expanduser("~/Documents/3_admin/banking"),
"audit-report": os.path.expanduser("~/2_KNOWLEDGE/audits/incoming"),
"contract": os.path.expanduser("~/Documents/3_admin/contracts"),
"tax-doc": os.path.expanduser("~/Documents/3_admin/tax"),
"code-or-tech-doc": os.path.expanduser("~/Documents/0_inbox"),
"screenshot-code": os.path.expanduser("~/Pictures/Screenshots"),
"screenshot-other": os.path.expanduser("~/Pictures/Screenshots"),
"photo": os.path.expanduser("~/Pictures/inbox"),
"other": None,
}
SYSTEM_PROMPT = (
"You classify documents and suggest concise filenames. Respond ONLY with valid JSON, "
"no reasoning text, no markdown fences. Schema: "
f"{{\"category\":\"<one of: {'|'.join(CATEGORIES)}>\",\"name\":\"<kebab-case-filename-no-extension-max-50-chars-ascii-only>\",\"confidence\":<0.0-1.0>}}"
)
def emit(payload):
print(json.dumps(payload))
sys.exit(0)
def extract_text(path: Path) -> tuple[str, str]:
"""Return (text, source) where source is 'pdftotext', 'ocrmac', 'head', or 'none'."""
ext = path.suffix.lower()
if ext == ".pdf":
try:
r = subprocess.run(
["/opt/homebrew/bin/pdftotext", "-q", "-layout", "-l", "3", str(path), "-"],
capture_output=True, text=True, timeout=10,
)
text = (r.stdout or "").strip()
if len(text) >= MIN_TEXT_CHARS:
return text[:MAX_TEXT_CHARS], "pdftotext"
# Fall through to OCR of rendered first page
tmp = Path(f"/tmp/classify-{os.getpid()}.png")
subprocess.run(
["/opt/homebrew/bin/pdftoppm", "-png", "-singlefile", "-r", "120", "-f", "1", "-l", "1", str(path), str(tmp.with_suffix(""))],
capture_output=True, timeout=15,
)
if tmp.exists():
try:
from ocrmac import ocrmac
chunks = ocrmac.OCR(str(tmp)).recognize()
ocr_text = " ".join(t for t, _, _ in chunks).strip()
tmp.unlink(missing_ok=True)
if len(ocr_text) >= MIN_TEXT_CHARS:
return ocr_text[:MAX_TEXT_CHARS], "pdf-ocr"
except Exception:
tmp.unlink(missing_ok=True)
except Exception:
pass
return "", "none"
if ext in (".jpg", ".jpeg", ".png", ".heic", ".webp", ".tiff", ".bmp"):
try:
from ocrmac import ocrmac
chunks = ocrmac.OCR(str(path)).recognize()
text = " ".join(t for t, _, _ in chunks).strip()
if len(text) >= MIN_TEXT_CHARS:
return text[:MAX_TEXT_CHARS], "ocrmac"
except Exception:
pass
# Image with no text — fall back to "photo" category without LLM call
return "", "image-no-text"
if ext in (".txt", ".md", ".csv", ".log", ".rtf"):
try:
text = path.read_text(errors="ignore")[:MAX_TEXT_CHARS].strip()
if len(text) >= MIN_TEXT_CHARS:
return text, "head"
except Exception:
pass
return "", "none"
return "", "unsupported-ext"
def classify_with_ollama(text: str) -> dict:
import urllib.request
payload = {
"model": OLLAMA_MODEL,
"format": "json",
"stream": False,
"options": {"temperature": 0.1, "num_predict": 150},
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": f"Text:\n{text}"},
],
}
req = urllib.request.Request(
OLLAMA_URL,
data=json.dumps(payload).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=60) as r:
body = json.loads(r.read())
content = body["message"]["content"].strip()
return json.loads(content)
def main():
if len(sys.argv) != 2:
emit({"category": "skip", "reason": "usage: classify-file.py <path>"})
path = Path(sys.argv[1]).expanduser().resolve()
if not path.is_file():
emit({"category": "skip", "reason": "not-a-file"})
if path.stat().st_size < 64:
emit({"category": "skip", "reason": "too-small"})
text, source = extract_text(path)
if source == "image-no-text":
# We can't classify visually right now (Qwen3-VL is unstable),
# so route to ~/Pictures/inbox conservatively.
emit({
"category": "photo",
"name": path.stem.lower().replace(" ", "-")[:50],
"confidence": 0.5,
"route": ROUTES["photo"],
"source": source,
})
if not text or len(text) < MIN_TEXT_CHARS:
emit({"category": "skip", "reason": f"no-text({source})"})
ext = path.suffix.lower()
file_kind = (
"screenshot/image (text extracted via OCR)" if source == "ocrmac"
else "scanned PDF (text via OCR of first page)" if source == "pdf-ocr"
else "PDF with embedded text" if source == "pdftotext"
else "plain text file"
)
hint = f"File-type hint: {file_kind}. Original filename: '{path.name}'."
try:
result = classify_with_ollama(hint + "\n\n" + text)
except Exception as e:
emit({"category": "skip", "reason": f"ollama-err: {type(e).__name__}: {e}"})
cat = result.get("category", "").strip()
if cat not in CATEGORIES:
emit({"category": "skip", "reason": f"invalid-category: {cat!r}"})
name = (result.get("name") or path.stem).strip()
name = "".join(c for c in name if c.isascii()).replace(" ", "-").lower()[:50] or path.stem.lower()
conf = float(result.get("confidence", 0))
emit({
"category": cat,
"name": name,
"confidence": conf,
"route": ROUTES.get(cat),
"source": source,
})
if __name__ == "__main__":
main()
#!/bin/zsh
# Weekly guard against ~/.claude/file-history growing unbounded
# (Claude Code Issue #10107: file-history can cascade to hundreds of GB).
#
# Behavior: measures ~/.claude/file-history; if size > MAX_GB, prunes oldest
# checkpoint directories until under threshold. Logs every run + posts a macOS
# notification if a prune actually fired.
#
# Schedule: weekly via ~/Library/LaunchAgents/com.adam.claude-file-history-guard.plist.
# Manual: zsh ~/.local-bin/claude-file-history-guard.sh
# Dry-run: DRY_RUN=1 zsh ~/.local-bin/claude-file-history-guard.sh
set -u
MAX_GB=${MAX_GB:-2}
TARGET="$HOME/.claude/file-history"
LOG="$HOME/.local-ai/logs/claude-file-history-guard.log"
DRY_RUN=${DRY_RUN:-0}
mkdir -p "$(dirname "$LOG")"
log() { print -P "[%D{%Y-%m-%d %H:%M:%S}] $1" >> "$LOG"; }
[[ -d "$TARGET" ]] || { log "no target ($TARGET); nothing to do"; exit 0; }
current_kb=$(du -sk "$TARGET" 2>/dev/null | awk '{print $1}')
current_kb=${current_kb:-0}
threshold_kb=$(( MAX_GB * 1024 * 1024 ))
current_gb_display=$(printf "%.2f" $(( current_kb / 1024.0 / 1024.0 )))
log "=== run start (DRY_RUN=$DRY_RUN, MAX_GB=$MAX_GB, current=${current_gb_display}GB) ==="
if (( current_kb <= threshold_kb )); then
log "OK: ${current_gb_display}GB <= ${MAX_GB}GB, no prune"
log "=== run end ==="
exit 0
fi
log "OVER: ${current_gb_display}GB > ${MAX_GB}GB, pruning oldest entries"
# Sort children by mtime (oldest first), prune until size <= threshold.
# Use -mindepth 1 -maxdepth 1 so we only delete top-level checkpoint dirs/files,
# never the target itself.
pruned=0
freed_kb=0
while IFS= read -r entry; do
[[ -e "$entry" ]] || continue
entry_kb=$(du -sk "$entry" 2>/dev/null | awk '{print $1}')
entry_kb=${entry_kb:-0}
if (( DRY_RUN )); then
log "DRY prune: $entry (${entry_kb}KB)"
else
if rm -rf -- "$entry" 2>>"$LOG"; then
log "pruned: $entry (${entry_kb}KB)"
freed_kb=$(( freed_kb + entry_kb ))
pruned=$(( pruned + 1 ))
else
log "FAILED to prune: $entry"
fi
fi
remaining_kb=$(du -sk "$TARGET" 2>/dev/null | awk '{print $1}')
remaining_kb=${remaining_kb:-0}
(( remaining_kb <= threshold_kb )) && break
done < <(
find -H "$TARGET" -mindepth 1 -maxdepth 1 \
-exec stat -f '%m %N' {} \; 2>/dev/null \
| sort -n \
| awk '{ $1=""; sub(/^ /,""); print }'
)
final_kb=$(du -sk "$TARGET" 2>/dev/null | awk '{print $1}')
final_kb=${final_kb:-0}
final_gb_display=$(printf "%.2f" $(( final_kb / 1024.0 / 1024.0 )))
freed_gb_display=$(printf "%.2f" $(( freed_kb / 1024.0 / 1024.0 )))
log "pruned $pruned entries, freed ${freed_gb_display}GB, now ${final_gb_display}GB"
# Notify only if we actually pruned (not dry-run, > 0)
if (( pruned > 0 && DRY_RUN == 0 )); then
/usr/bin/osascript -e "display notification \"Pruned ${pruned} entries, freed ${freed_gb_display}GB. Now ${final_gb_display}GB / ${MAX_GB}GB cap.\" with title \"Claude file-history guard\"" 2>/dev/null
fi
log "=== run end ==="
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.adam.adhd-cleanup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/adam/.local-bin/adhd-cleanup.sh</string>
</array>
<key>StartInterval</key>
<integer>600</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/adam/.local-ai/logs/adhd-cleanup.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/adam/.local-ai/logs/adhd-cleanup.stderr.log</string>
<key>ProcessType</key>
<string>Background</string>
<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>10</integer>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.adam.claude-file-history-guard</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/adam/.local-bin/claude-file-history-guard.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Weekday</key>
<integer>0</integer>
<key>Hour</key>
<integer>3</integer>
<key>Minute</key>
<integer>15</integer>
</dict>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/adam/.local-ai/logs/claude-file-history-guard.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/adam/.local-ai/logs/claude-file-history-guard.stderr.log</string>
<key>ProcessType</key>
<string>Background</string>
<key>LowPriorityIO</key>
<true/>
<key>Nice</key>
<integer>15</integer>
</dict>
</plist>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.adam.nexus</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/adam/.local-ai/start-all.sh</string>
<string>--no-open</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>ProcessType</key>
<string>Background</string>
<key>StandardOutPath</key>
<string>/Users/adam/.local-ai/logs/launchagent.log</string>
<key>StandardErrorPath</key>
<string>/Users/adam/.local-ai/logs/launchagent.log</string>
</dict>
</plist>
#!/bin/zsh
# Waits for the MLX coder server on :1234, then smoke-tests + benchmarks prefill/decode tok/s.
set -u
LOG=~/.local-ai/logs/bench-1234.log
: > "$LOG"
echo "[$(date +%T)] waiting for :1234 ..." | tee -a "$LOG"
# Wait up to ~4h for the model to download + server to come up (slow link).
for i in {1..2880}; do
if curl -s --max-time 4 http://localhost:1234/v1/models >/dev/null 2>&1; then
break
fi
sleep 5
done
MODELS_JSON=$(curl -s --max-time 8 http://localhost:1234/v1/models 2>/dev/null)
if [ -z "$MODELS_JSON" ]; then
echo "[$(date +%T)] FAIL: :1234 never came up. Last log lines:" | tee -a "$LOG"
tail -n 25 ~/.local-ai/logs/vllm-mlx-1234.log | tee -a "$LOG"
exit 1
fi
echo "[$(date +%T)] /v1/models => $MODELS_JSON" | tee -a "$LOG"
MODEL_ID=$(echo "$MODELS_JSON" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['data'][0]['id'])" 2>/dev/null)
echo "[$(date +%T)] served model id: $MODEL_ID" | tee -a "$LOG"
# --- Smoke ---
echo "[$(date +%T)] SMOKE:" | tee -a "$LOG"
curl -s --max-time 120 http://localhost:1234/v1/chat/completions -H 'Content-Type: application/json' \
-d "{\"model\":\"$MODEL_ID\",\"messages\":[{\"role\":\"user\",\"content\":\"Write a one-line Python function that reverses a string.\"}],\"max_tokens\":80}" \
| tee -a "$LOG"
echo "" | tee -a "$LOG"
# --- Decode benchmark: short prompt, 256 tokens ---
echo "[$(date +%T)] DECODE bench (short prompt, 256 new tokens):" | tee -a "$LOG"
python3 - "$MODEL_ID" <<'PY' | tee -a "$LOG"
import sys, json, time, urllib.request
model=sys.argv[1]
def call(prompt, max_tokens):
body=json.dumps({"model":model,"messages":[{"role":"user","content":prompt}],"max_tokens":max_tokens,"temperature":0.0}).encode()
req=urllib.request.Request("http://localhost:1234/v1/chat/completions",data=body,headers={"Content-Type":"application/json"})
t=time.time(); r=json.load(urllib.request.urlopen(req,timeout=600)); dt=time.time()-t
u=r.get("usage",{})
return dt,u.get("prompt_tokens",0),u.get("completion_tokens",0)
# decode: trivial prompt, generate 256
dt,pt,ct=call("Count slowly: 1, 2, 3, and keep writing a long story about an M5 chip.",256)
print(f" decode: {ct} completion tokens in {dt:.2f}s => {ct/dt:.1f} tok/s (prompt_tokens={pt})")
# prefill: long prompt (~3000 tokens), generate just 8
longp=("Analyze the following text and summarize in one sentence.\n\n"+("The quick brown fox jumps over the lazy dog. "*450))
dt2,pt2,ct2=call(longp,8)
print(f" prefill: {pt2} prompt tokens processed in {dt2:.2f}s (gen {ct2}) => ~{pt2/dt2:.0f} prompt tok/s (incl. {ct2} decode)")
PY
echo "[$(date +%T)] DONE." | tee -a "$LOG"
#!/bin/zsh
# Waits for the heavy coder download to finish, then swaps :1234 from the 1.7B to the
# heavy Qwen3-Coder-Next (served as qwen3-coder-mlx, so opencode/Open WebUI keep working).
# Only touches the coder venv's serve (.venvs/vllm-mlx/bin) — leaves the :8000 vision server alone.
LOG=~/.local-ai/logs/coder-swap.log; : > "$LOG"
echo "[$(date '+%F %T')] waiting for heavy coder download to finish..." >> "$LOG"
sleep 30
while pgrep -f "hf download Eldadalbajob" >/dev/null 2>&1; do sleep 30; done
sleep 5
echo "[$(date '+%F %T')] download finished; restarting :1234 with the heavy coder" >> "$LOG"
pkill -f ".venvs/vllm-mlx/bin/vllm-mlx serve" 2>/dev/null
pkill -f "coder-up.sh" 2>/dev/null
sleep 3
nohup /bin/zsh "$HOME/.local-ai/coder-up.sh" >/dev/null 2>&1 &
echo "[$(date '+%F %T')] coder-up relaunched (heavy model loads ~45GB; ready in ~1 min)" >> "$LOG"
#!/bin/zsh
# Resilient chain: download the abliterated MLX coder (retry until complete), then serve on :1234.
# Survives session end (launched via nohup). Logs to ~/.local-ai/logs/.
export HF_HUB_DISABLE_XET=1 # classic HTTPS LFS — Xet stalls unauthenticated
REPO=Eldadalbajob/Huihui-Qwen3-Coder-Next-abliterated-mlx-4Bit # heavy abliterated MLX coder (44.9 GB). Fast fallback: mlx-community/Josiefied-Qwen3-1.7B-abliterated-v1-4bit
BIN=~/.venvs/vllm-mlx/bin
LOG=~/.local-ai/logs/coder-up.log
echo "[$(date '+%F %T')] coder-up start; repo=$REPO" >> "$LOG"
for i in {1..80}; do
if "$BIN/hf" download "$REPO" >> ~/.local-ai/logs/dl-coder.log 2>&1; then
echo "[$(date '+%F %T')] download complete on attempt $i" >> "$LOG"
break
fi
echo "[$(date '+%F %T')] download attempt $i incomplete (slow link); resuming in 15s" >> "$LOG"
sleep 15
done
echo "[$(date '+%F %T')] starting vllm-mlx serve on :1234" >> "$LOG"
exec "$BIN/vllm-mlx" serve "$REPO" --served-model-name qwen3-coder-mlx --port 1234 \
--continuous-batching --enable-prefix-cache --enable-metrics \
>> ~/.local-ai/logs/vllm-mlx-1234.log 2>&1
#!/usr/bin/env python3
"""A1111-compatibility shim for Draw Things.
Open WebUI's automatic1111 image engine expects a full AUTOMATIC1111 /sdapi/v1 API.
Draw Things (:7860) implements txt2img + options but with its OWN schema (no
'sd_model_checkpoint') and 404s on sd-models/samplers/progress. This proxy sits on
:7861, forwards to DT, and patches exactly the two spots Open WebUI v0.9.5 reads:
* GET /sdapi/v1/options -> inject 'sd_model_checkpoint' (else KeyError -> HTTP 400)
* POST /sdapi/v1/txt2img -> ensure response has 'info' (else KeyError after images)
Plus harmless stubs for sd-models/samplers/progress and a pass-through for anything else.
No third-party deps (stdlib only).
"""
import json, urllib.request, urllib.error
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
DT = "http://localhost:7860"
PORT = 7861
def dt(method, path, body=None, timeout=600):
req = urllib.request.Request(DT + path, data=body, method=method)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req, timeout=timeout) as r:
return r.status, r.read()
class H(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a):
pass
def _send(self, code, obj=None, raw=None):
data = raw if raw is not None else json.dumps({} if obj is None else obj).encode()
self.send_response(code)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
p = self.path.split("?", 1)[0]
if p == "/sdapi/v1/options":
try:
_, body = dt("GET", "/sdapi/v1/options", timeout=15)
opt = json.loads(body)
except Exception:
opt = {}
opt.setdefault("sd_model_checkpoint", opt.get("model") or "DrawThings")
return self._send(200, opt)
if p == "/sdapi/v1/sd-models":
return self._send(200, [{"title": "DrawThings", "model_name": "DrawThings",
"hash": None, "sha256": None, "filename": None, "config": None}])
if p == "/sdapi/v1/samplers":
return self._send(200, [{"name": n, "aliases": [], "options": {}}
for n in ["Euler a", "Euler", "DPM++ 2M", "DDIM"]])
if p == "/sdapi/v1/progress":
return self._send(200, {"progress": 0.0, "eta_relative": 0.0, "state": {},
"current_image": None, "textinfo": None})
try:
st, body = dt("GET", p, timeout=30)
return self._send(st, raw=body)
except urllib.error.HTTPError as e:
return self._send(e.code, raw=e.read())
except Exception:
return self._send(200, {})
def do_POST(self):
p = self.path.split("?", 1)[0]
n = int(self.headers.get("Content-Length") or 0)
body = self.rfile.read(n) if n else None
if p == "/sdapi/v1/options":
return self._send(200, {}) # no-op; Open WebUI swallows failures here
if p == "/sdapi/v1/txt2img":
try:
_, out = dt("POST", "/sdapi/v1/txt2img", body=body, timeout=600)
res = json.loads(out)
except Exception as e:
return self._send(500, {"error": str(e)})
res.setdefault("info", "{}")
res.setdefault("parameters", {})
return self._send(200, res)
try:
st, out = dt("POST", p, body=body, timeout=600)
return self._send(st, raw=out)
except urllib.error.HTTPError as e:
return self._send(e.code, raw=e.read())
except Exception:
return self._send(200, {})
if __name__ == "__main__":
ThreadingHTTPServer(("127.0.0.1", PORT), H).serve_forever()
#!/usr/bin/env python3
# Nexus app icon — minimalist Apple-style: rounded-square, indigo->violet diagonal
# gradient, subtle top sheen, white "network hub" glyph (central node + 6 satellites).
import math, os
import numpy as np
from PIL import Image, ImageDraw
S = 1024 # final size
SS = 2 # supersample factor
W = S * SS
# --- diagonal gradient (top-left light indigo -> bottom-right deep violet) ---
top = np.array([124, 140, 255], dtype=float) # #7C8CFF
bot = np.array([ 58, 47, 150], dtype=float) # #3A2F96
xs = np.linspace(0, 1, W)
gx, gy = np.meshgrid(xs, xs)
t = (gx + gy) / 2.0
arr = (top[None, None, :] + (bot - top)[None, None, :] * t[:, :, None]).astype("uint8")
grad = Image.fromarray(arr, "RGB").convert("RGBA")
# --- rounded-square mask (Apple ~22.37% corner radius) ---
mask = Image.new("L", (W, W), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, W - 1, W - 1], radius=int(0.2237 * W), fill=255)
img = Image.new("RGBA", (W, W), (0, 0, 0, 0))
img.paste(grad, (0, 0), mask)
# --- subtle top sheen for depth ---
overlay = Image.new("RGBA", (W, W), (0, 0, 0, 0))
ImageDraw.Draw(overlay).ellipse(
[int(-0.25 * W), int(-0.62 * W), int(1.25 * W), int(0.42 * W)], fill=(255, 255, 255, 26))
overlay = Image.composite(overlay, Image.new("RGBA", (W, W), (0, 0, 0, 0)), mask)
img = Image.alpha_composite(img, overlay)
# --- network-hub glyph ---
d = ImageDraw.Draw(img)
cx = cy = W // 2
R = int(0.225 * W) # satellite ring radius
nc = int(0.070 * W) # central node radius
ns = int(0.042 * W) # satellite radius
lw = int(0.013 * W) # spoke width
pts = []
for k in range(6):
a = math.radians(60 * k - 90)
pts.append((cx + R * math.cos(a), cy + R * math.sin(a)))
# faint hexagon ring connecting satellites
for i in range(6):
x1, y1 = pts[i]; x2, y2 = pts[(i + 1) % 6]
d.line([x1, y1, x2, y2], fill=(255, 255, 255, 110), width=int(lw * 0.7))
# spokes center -> satellites
for (x, y) in pts:
d.line([cx, cy, x, y], fill=(255, 255, 255, 235), width=lw)
# satellite nodes
for (x, y) in pts:
d.ellipse([x - ns, y - ns, x + ns, y + ns], fill=(255, 255, 255, 255))
# central node (with a faint halo)
d.ellipse([cx - nc * 1.5, cy - nc * 1.5, cx + nc * 1.5, cy + nc * 1.5], fill=(255, 255, 255, 40))
d.ellipse([cx - nc, cy - nc, cx + nc, cy + nc], fill=(255, 255, 255, 255))
# --- downscale (anti-alias) + save ---
out = img.resize((S, S), Image.LANCZOS)
path = os.path.expanduser("~/.local-ai/nexus-1024.png")
out.save(path)
print("wrote", path, out.size)
#!/bin/zsh
# Prints a valid Open WebUI bearer token (signed with the local secret) for the admin user.
python3 - <<'PY'
import hmac,hashlib,base64,json,sqlite3,os
sec=open(os.path.expanduser('~/.open-webui/.webui_secret_key')).read().strip()
u=sqlite3.connect(os.path.expanduser('~/.open-webui/webui.db')).execute("select id from user limit 1").fetchone()
b=lambda x:base64.urlsafe_b64encode(x).rstrip(b'=')
h=b(b'{"alg":"HS256","typ":"JWT"}');p=b(json.dumps({"id":u[0]},separators=(',',':')).encode())
print((h+b'.'+p+b'.'+b(hmac.new(sec.encode(),h+b'.'+p,hashlib.sha256).digest())).decode())
PY
#!/bin/zsh
# Nexus — launch the whole local-AI stack, then open the UI.
# Idempotent: only starts what isn't already running. Safe to run repeatedly.
export PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
LOG="$HOME/.local-ai/logs"; mkdir -p "$LOG"
DOCKER=/Applications/OrbStack.app/Contents/MacOS/xbin/docker
up() { curl -s --max-time 3 "$1" >/dev/null 2>&1; }
note() { echo "[$(date +%T)] $1" >> "$LOG/start-all.log"; }
: > "$LOG/start-all.log"; note "Nexus start"
# 1) Ollama (brew service)
up http://localhost:11434/api/tags || { note "starting ollama"; brew services start ollama >/dev/null 2>&1; }
# 2) MLX coder server :1234 (download-then-serve chain; serves instantly if cached)
up http://localhost:1234/v1/models || { note "starting MLX :1234"; nohup /bin/zsh "$HOME/.local-ai/coder-up.sh" >/dev/null 2>&1 & }
# 2b) MLX vision server :8000 — Qwen3-VL (image understanding; model cached). Offline = no online checks.
up http://localhost:8000/v1/models || { note "starting MLX vision :8000"; nohup env HF_HUB_OFFLINE=1 HF_HUB_DISABLE_XET=1 "$HOME/.venvs/vllm-mlx-vision/bin/vllm-mlx" serve mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit --port 8000 > "$LOG/vllm-mlx-vision-8000.log" 2>&1 & }
# 3) mlx-audio TTS :8001
/usr/sbin/lsof -nP -iTCP:8001 -sTCP:LISTEN >/dev/null 2>&1 || { note "starting mlx-audio :8001"; cd "$LOG" && nohup env HF_HUB_OFFLINE=1 HF_HUB_DISABLE_XET=1 "$HOME/.local/bin/mlx_audio.server" --port 8001 --log-dir "$LOG" > "$LOG/mlx-audio-8001.log" 2>&1 & cd - >/dev/null; }
# 4) OrbStack engine + SearXNG :8888
if ! "$DOCKER" version >/dev/null 2>&1; then
note "starting OrbStack"; open -a OrbStack
for i in {1..45}; do "$DOCKER" version >/dev/null 2>&1 && break; sleep 2; done
fi
up "http://localhost:8888/search?q=x&format=json" || { note "starting SearXNG"; "$DOCKER" start searxng >/dev/null 2>&1; }
# 4b) Draw Things A1111-compat shim :7861 (lets Open WebUI image gen drive Draw Things :7860)
up http://localhost:7861/sdapi/v1/sd-models || { note "starting DT shim :7861"; nohup python3 "$HOME/.local-ai/dt-a1111-shim.py" > "$LOG/dt-shim-7861.log" 2>&1 & }
# 5) Open WebUI :8080
up http://localhost:8080/health || { note "starting Open WebUI :8080"; nohup /bin/zsh "$HOME/.local-ai/webui-up.sh" > "$LOG/open-webui-8080.log" 2>&1 & }
# wait for the UI, then open it
note "waiting for :8080"
for i in {1..150}; do up http://localhost:8080/health && break; sleep 2; done
if [[ "$1" != "--no-open" && "$NEXUS_NO_OPEN" != "1" ]]; then
note "opening browser"; open "http://localhost:8080"
fi
note "done"
#!/bin/zsh
# Nexus — stop the local-AI stack cleanly. Leaves the Ollama service running
# (it's the lightweight base) but unloads its models to free RAM.
export PATH=/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
LOG="$HOME/.local-ai/logs"; mkdir -p "$LOG"
DOCKER=/Applications/OrbStack.app/Contents/MacOS/xbin/docker
note() { echo "[$(date +%T)] $1" >> "$LOG/stop-all.log"; }
: > "$LOG/stop-all.log"; note "Nexus stop"
pkill -f "open-webui serve" 2>/dev/null && note "stopped Open WebUI"
pkill -f "coder-up.sh" 2>/dev/null
pkill -f "vision-up.sh" 2>/dev/null
pkill -f "bench-1234.sh" 2>/dev/null
pkill -f "vllm-mlx serve" 2>/dev/null && note "stopped MLX servers"
pkill -f "mlx_audio.server" 2>/dev/null && note "stopped TTS"
"$DOCKER" stop searxng >/dev/null 2>&1 && note "stopped SearXNG"
# free Ollama RAM by unloading loaded models (keep the service itself running)
for m in $(ollama ps 2>/dev/null | awk 'NR>1{print $1}'); do ollama stop "$m" >/dev/null 2>&1; done
note "done"
osascript -e 'display notification "All local-AI services stopped." with title "Nexus" sound name "Submarine"' 2>/dev/null
#!/bin/zsh
# Headless test of Open WebUI v0.9.5 SearXNG web search.
# Usage: TOKEN=<your_api_key_or_jwt> ./test-websearch.sh
# Get TOKEN from: Open WebUI -> Settings -> Account -> API Keys (or copy the JWT from browser localStorage).
set -e
: "${TOKEN:?Set TOKEN to a bearer token (API key from Settings > Account, or a JWT)}"
BASE="${BASE:-http://127.0.0.1:8080}"
echo "=== 0. SearXNG itself returns JSON results ==="
curl -s "http://localhost:8888/search?q=apple+m5+max&format=json" \
| python3 -c "import sys,json;d=json.load(sys.stdin);print('searxng results:',len(d.get('results',[])))"
echo "=== 1. Server-side web search endpoint (the real test) ==="
# Required body = SearchForm: {"queries": [ ... ]} (queries is an ARRAY, required)
curl -sS -X POST "$BASE/api/v1/retrieval/process/web/search" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"queries": ["what is the Apple M5 Max chip"]}' \
-w "\nHTTP %{http_code}\n"
echo "=== 2. (optional) End-to-end via chat completions with web_search feature ==="
curl -sS -X POST "$BASE/api/chat/completions" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"qwen3-coder-mlx","messages":[{"role":"user","content":"What is todays date in London?"}],"stream":false,"features":{"web_search":true}}' \
-w "\nHTTP %{http_code}\n" | python3 -c "import sys,json;d=json.load(sys.stdin);print('has sources:', bool(d.get('sources')))" 2>/dev/null || true
#!/usr/bin/env python3
"""Vision MCP server for opencode/crush — reads local images with the local Qwen3-VL model.
opencode's native image attachment to custom OpenAI-compatible providers is broken
(anomalyco/opencode#20802), so this MCP tool is the reliable offline path: it base64-encodes
a local image and POSTs an OpenAI multimodal (image_url) request to the local vision server
at :8000, returning the model's text answer. Fully offline (localhost only).
"""
import base64, json, os, urllib.request
from mcp.server.fastmcp import FastMCP
VISION_URL = "http://localhost:8000/v1/chat/completions"
MODEL = "mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit"
mcp = FastMCP("vision")
@mcp.tool()
def describe_image(path: str, question: str = "Describe this image in detail.") -> str:
"""Look at / read a LOCAL image file and answer a question about it using the local
vision model. `path` = path to an image file (png/jpg/webp/gif). `question` = what to ask
about the image (e.g. 'what does this UI mockup show?', 'transcribe the text', 'what's wrong?')."""
p = os.path.expanduser(path.strip())
if not os.path.isfile(p):
return f"ERROR: file not found: {p}"
ext = (os.path.splitext(p)[1].lstrip(".") or "png").lower()
if ext == "jpg":
ext = "jpeg"
with open(p, "rb") as f:
data_uri = f"data:image/{ext};base64," + base64.b64encode(f.read()).decode()
body = json.dumps({
"model": MODEL,
"messages": [{"role": "user", "content": [
{"type": "text", "text": question},
{"type": "image_url", "image_url": {"url": data_uri}},
]}],
"max_tokens": 1024, "temperature": 0.2,
}).encode()
req = urllib.request.Request(VISION_URL, data=body, headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req, timeout=300) as r:
d = json.load(r)
return d["choices"][0]["message"]["content"]
except Exception as e:
return f"ERROR calling local vision model at {VISION_URL}: {e}. Is the :8000 server running? (start-all.sh starts it)"
if __name__ == "__main__":
mcp.run()
#!/bin/zsh
# Resilient chain for the Qwen3-VL vision backend on :8000.
# Waits until the coder backend (:1234) is live so the two big downloads don't fight the slow
# link, then downloads (retry-until-complete) and serves the vision model. Survives session end.
export HF_HUB_DISABLE_XET=1
REPO=mlx-community/Qwen3-VL-30B-A3B-Instruct-8bit
BIN=~/.venvs/vllm-mlx-vision/bin
LOG=~/.local-ai/logs/vision-up.log
echo "[$(date '+%F %T')] vision-up start; waiting for coder :1234 to be live first" >> "$LOG"
for i in {1..5760}; do
curl -s --max-time 4 http://localhost:1234/v1/models >/dev/null 2>&1 && break
sleep 15
done
echo "[$(date '+%F %T')] coder :1234 up — beginning vision download" >> "$LOG"
for i in {1..120}; do
if "$BIN/hf" download "$REPO" >> ~/.local-ai/logs/dl-vision.log 2>&1; then
echo "[$(date '+%F %T')] vision download complete on attempt $i" >> "$LOG"
break
fi
echo "[$(date '+%F %T')] vision download attempt $i incomplete (slow link); resuming in 15s" >> "$LOG"
sleep 15
done
echo "[$(date '+%F %T')] starting vllm-mlx vision serve on :8000" >> "$LOG"
exec "$BIN/vllm-mlx" serve "$REPO" --port 8000 \
>> ~/.local-ai/logs/vllm-mlx-vision-8000.log 2>&1
#!/bin/zsh
# Open WebUI launcher — best local config:
# - auth ON with a persistent secret key (log in once, stay logged in), bound to 127.0.0.1 only
# - telemetry OFF; wired to Ollama (:11434) + MLX (:1234)
# - web search (SearXNG :8888), audio (mlx-audio :8001 TTS+STT), image-gen (Draw Things :7860)
export DATA_DIR="$HOME/.open-webui"
mkdir -p "$DATA_DIR"
SECRET_FILE="$DATA_DIR/.webui_secret_key"
[ -f "$SECRET_FILE" ] || openssl rand -hex 32 > "$SECRET_FILE"
export WEBUI_SECRET_KEY="$(cat "$SECRET_FILE")"
# Auth stays ON, but the persistent WEBUI_SECRET_KEY above keeps your session valid
# across restarts → you log in ONCE (adx93@icloud.com) and stay logged in.
export JWT_EXPIRES_IN=-1 # token never expires (no surprise logouts)
# Ollama via its NATIVE API (no dupes); the OpenAI slot is reserved for the MLX backend only.
export ENABLE_OLLAMA_API=true
export OLLAMA_BASE_URL="http://localhost:11434"
export ENABLE_OPENAI_API=true
# MLX coder (:1234) + MLX vision Qwen3-VL (:8000, when running). Ollama is on its native API above.
export OPENAI_API_BASE_URLS="http://localhost:1234/v1;http://localhost:8000/v1"
export OPENAI_API_KEYS="not-required;not-required"
export ANONYMIZED_TELEMETRY=false
export DO_NOT_TRACK=true
export SCARF_NO_ANALYTICS=true
export OFFLINE_MODE=false # internet allowed (required for web search)
export ENABLE_VERSION_UPDATE_CHECK=false # ...but still no version pings
# --- Web search via local SearXNG (names verified vs Open WebUI 0.9.5 source) ---
export ENABLE_WEB_SEARCH=true
export WEB_SEARCH_ENGINE=searxng
export SEARXNG_QUERY_URL="http://localhost:8888/search?q=<query>" # <query> token required; format=json auto-added
export WEB_SEARCH_RESULT_COUNT=5
export WEB_SEARCH_CONCURRENT_REQUESTS=10
# Feed results straight to the model (no embedding/vectorDB dependency -> no silent "empty" failures):
export BYPASS_WEB_SEARCH_EMBEDDING_AND_RETRIEVAL=true
export BYPASS_WEB_SEARCH_WEB_LOADER=true # use SearXNG snippets, skip per-page fetch (faster/robust)
export ENABLE_WEB_LOADER_SSL_VERIFICATION=true
# --- Audio: TTS + STT via local mlx-audio (:8001, OpenAI-compatible) ---
export AUDIO_TTS_ENGINE=openai
export AUDIO_TTS_OPENAI_API_BASE_URL="http://localhost:8001/v1"
export AUDIO_TTS_OPENAI_API_KEY="not-required"
export AUDIO_TTS_MODEL="mlx-community/Kokoro-82M-bf16"
export AUDIO_TTS_VOICE="af_heart"
# STT: Open WebUI's built-in local faster-whisper. (mlx-audio's parakeet transcribe route is
# broken in this build: "No module named mlx_audio.tts.models.parakeet_tdt_0".) Small model, downloads once.
export AUDIO_STT_ENGINE=""
export WHISPER_MODEL="base"
# --- Image generation via Draw Things (:7860) through the A1111-compat shim (:7861) ---
# DT's API omits sd_model_checkpoint; ~/.local-ai/dt-a1111-shim.py patches that so Open WebUI works.
# Requires once: Draw Things.app -> Advanced -> API Server ON, Protocol=HTTP, Port=7860.
export ENABLE_IMAGE_GENERATION=true
export IMAGE_GENERATION_ENGINE=automatic1111
export AUTOMATIC1111_BASE_URL="http://localhost:7861"
export IMAGE_SIZE=512x512
export IMAGE_STEPS=8
# (Leave IMAGE_GENERATION_MODEL unset — select the model inside Draw Things; the A1111 override is flaky.)
exec uvx --python 3.11 open-webui@latest serve --host 127.0.0.1 --port 8080
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>local.adam.nexus</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>-a</string>
<string>/Applications/Nexus.app</string>
</array>
<key>RunAtLoad</key> <true/>
<key>KeepAlive</key> <false/>
<key>ProcessType</key> <string>Interactive</string>
</dict>
</plist>

M5 Max — Session Handoff (2026-05-27)

Handoff for a peer Claude session on another machine. Captures what this session did, the current verified state, and the gotchas worth knowing before you touch anything.

Operator: Adam Boudjemaa boudjemaa.adam@gmail.com · GitHub Aboudjem Machine: MacBook M5 Max 14" 128 GB · macOS Tahoe 26.5 (build 25F71) · arm64 Gist: https://gist.github.com/Aboudjem/fad46568b34a09bf453fd48a57123252 · clone at ~/m5-setup


Current verified state

~/m5-setup/verify.sh132 PASS / 0 FAIL / 0 WARN (run it via zsh ./verify.sh — the file's tracked mode is non-exec, so ./verify.sh returns exit 126).

  • Foundry forge/cast/anvil 1.7.1 (installed via curl~/.foundry/bin, NOT brew — docs match this)
  • Ollama up: qwen3:30b, qwen3-coder:30b, nomic-embed-text:latest, qwen3:235b-thinking
  • whisper-cpp model healthy (~/.cache/whisper-cpp/ggml-large-v3-turbo.bin, 1.51 GB) — transcription smoke-tested
  • mise: node 24.16.0 / python 3.13.13 / rust stable / go 1.26.3
  • GPU cap iogpu.wired_limit_mb = 122880 (120 GB) — persisted via LaunchDaemon /Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist
  • MLX Metal True from the per-project venv ~/1_PROJECTS/mlx-lab/.venv
  • GitHub SSH auth works (key ~/.ssh/id_ed25519, registered as both auth + signing keys)
  • git commit signing ON (SSH, plain ssh-keygen, no 1Password dependency)
  • Claude Code 2.1.152, model pinned claude-opus-4-7, 4 generic MCPs connected

Claude Code config (~/.claude/settings.json)

  • model: claude-opus-4-7 · env: DISABLE_NONESSENTIAL_TRAFFIC=1, MAX_TOOL_USE_CONCURRENCY=15, EFFORT_LEVEL=xhigh, DISABLE_TERMINAL_TITLE=1
  • permissions: allow [Bash, Read, Edit, Write, Grep, Glob] + deny/ask security rules · theme dark
  • MCPs (user scope): filesystem, context7 (http), sequential-thinking, github — all ✓ Connected. brave-search skipped (no BRAVE_API_KEY).

Machine config touched this session

  • ~/.zshrc: has cc alias + CLAUDE_CODE_* env vars (pre-existing); ADDED export GITHUB_PERSONAL_ACCESS_TOKEN="$(gh auth token)" (resolves from gh keyring at shell start — no plaintext token on disk) and mlx_max/mlx_revert helpers.
  • ~/.gitconfig: ADDED gpg.format=ssh, user.signingkey=~/.ssh/id_ed25519.pub, commit.gpgsign=true. (Backup ~/.gitconfig.bak-*.)
  • Configs written from setup.sh templates: ~/.config/{starship.toml, ghostty/config + themes/neon-noir, atuin/config.toml}, ~/.ssh/config github stanza.

Gist commits pushed this session (over SSH, fast-forward, no force)

  • 82bf238 — model pin claude-opus-4-7-20260416claude-opus-4-7 (setup.sh + adam-mac-setup.md). The dated snapshot does not exist (claude rejects it).
  • cd0a45d — verify.sh MLX check false-positive → probe the per-project venv instead of global python.

(Drifts I found for the ollama :latest matcher and GPU-LaunchDaemon check were already fixed upstream before I got there — the gist is actively maintained from multiple machines.)

Software removed (each with explicit per-item consent)

procs, bottom, httpie (formulae) · mstystudio, notion, notion-calendar, slack, signal (casks). brew cleanup freed ~5.6 GB. Note: the gist's current spec (verify.sh DROPPED_CASKS) treats notion/notion-calendar/slack/signal as dropped — that's why they were removed.

GOTCHAS — read before acting

  1. Model IDs: use bare claude-opus-4-7 (Opus 4.7) / claude-sonnet-4-6 / claude-haiku-4-5. Dated suffixes like -20260416 do NOT resolve.
  2. gh is flaky in automated/sandboxed shells here — keychain access intermittently fails with "failed to change group ID: operation not permitted" (worsened by the $(gh auth token) zshrc line running gh at every shell init). For gist writes, use git over SSH (git push git@gist.github.com:<id>.git), NOT gh gist edit. Network commands need the sandbox disabled.
  3. The gist is multi-machine maintained — ALWAYS git fetch + rebase/reset onto remote before editing; my first push was rejected because the remote was 19 commits ahead. Never force-push.
  4. ANTHROPIC_BASE_URL must stay unsetclaude CLI stays on Anthropic cloud (Claude Max). Local models (Ollama @ localhost:11434) drive opencode/Cursor only.
  5. whisper-cpp ONLY (never pip install openai-whisper). iogpu cap = 122880 (NEVER 124000).
  6. Foundry stays curl-installed (~/.foundry/bin); don't "migrate to brew" — docs intentionally match the curl reality.

Local-only artifacts (on the M5, NOT in this gist — won't reach you)

This handoff is the portable summary. Full detail lives in /tmp on the M5: m5-audit-20260527.md (initial audit, 108/18/9), m5-install-fix.log (every install cmd+exit), m5-verify-20260527.md (8/8 smoke tests), m5-debug-20260527-2315.md (model-pin root cause), plus timestamped verify-*.log runs (1818 = 18 FAIL → 2343 = 0 FAIL/0 WARN).

Open / optional (nothing blocking)

  • Ghostty GUI behaviors await manual confirmation in a live tab (cmd+` quick terminal, cmd+shift+p command palette, dynamic tab title) — config is correct, just can't be exercised headlessly.
  • git signing uses plain ssh-keygen; setup.sh's spec prefers 1Password op-ssh-sign — switch by setting gpg.ssh.program once 1Password SSH agent is configured.
  • GUI app logins still manual: 1Password, Cursor, and license apps (Hazel, CleanShot, Cold Turkey).
#!/usr/bin/env zsh
# ============================================================
# Adam M5 Max — fully automated setup
#
# Usage (one command):
# ./setup.sh
#
# What it does (idempotent — safe to re-run):
# 0. Checks prerequisites (Xcode CLT, Homebrew)
# 1. Creates folder structure (numbered ~/N_* dirs)
# 2. Runs brew bundle install (Brewfile must be in same dir)
# 3. Pins mise language versions + activates in this shell
# 4. Installs npm globals (after Node exists)
# 5. Installs Python tools via uv (Slither, Halmos, Wake)
# 6. Installs Foundry via official curl installer
# 7. Writes ALL config files (~/.zshrc, ~/.gitconfig, ~/.config/*)
# 8. Generates SSH key + uploads to GitHub (incl. SSH agent + Keychain)
# 9. Sets macOS defaults (Finder, dock, key repeat)
# 10. Disables Spotlight ⌘+Space hotkey
# 11. Starts Ollama + pulls base models in background
# 12. Downloads Whisper model with size verification
# 13. Sets GPU memory cap (sysctl live + LaunchDaemon for reboot persistence)
# 14. Scaffolds MLX project + Python venv
# 15. Wires fzf keybindings
# (then prints remaining manual steps — OAuth logins, license keys)
# ============================================================
set -u # error on unset vars (no `set -e` — we handle errors per-step)
# Colors
C_GREEN='\033[0;32m'
C_BLUE='\033[0;34m'
C_YELLOW='\033[0;33m'
C_RED='\033[0;31m'
C_RESET='\033[0m'
step() { echo -e "\n${C_BLUE}▶ $1${C_RESET}"; }
ok() { echo -e " ${C_GREEN}✓${C_RESET} $1"; }
warn() { echo -e " ${C_YELLOW}⚠${C_RESET} $1"; }
err() { echo -e " ${C_RED}✗${C_RESET} $1"; }
# Backup an existing file before overwriting
backup_if_exists() {
local file="$1"
if [ -f "$file" ]; then
cp "$file" "${file}.bak-$(date +%s)"
warn "Backed up existing $file → ${file}.bak-*"
fi
}
# ============================================================
# 0. Prerequisites
# ============================================================
step "0. Checking prerequisites"
if ! xcode-select -p &> /dev/null; then
err "Xcode CLI tools not installed. Run: xcode-select --install"
exit 1
fi
ok "Xcode CLI tools ready"
if ! command -v brew &> /dev/null; then
err "Homebrew not installed. Run: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\""
exit 1
fi
ok "Homebrew ready"
if [ "$(uname -m)" != "arm64" ]; then
err "This script is for Apple Silicon Macs (M1/M2/M3/M4/M5)"
exit 1
fi
ok "Apple Silicon detected"
# ============================================================
# 1. Folder structure (numbered for ADHD Finder ordering)
# ============================================================
step "1. Creating folder structure"
mkdir -p ~/0_INBOX ~/1_PROJECTS ~/2_KNOWLEDGE ~/3_ADMIN ~/9_ARCHIVE ~/backup
mkdir -p ~/Pictures/Screenshots ~/.config
mkdir -p ~/.config/{ghostty,atuin,mise} ~/.claude
ok "Folder tree ready"
# ============================================================
# 2. Brewfile install (all CLI + GUI apps)
# ============================================================
step "2. Installing everything from Brewfile"
if [ -f "$(dirname "$0")/Brewfile" ]; then
brew bundle install --file="$(dirname "$0")/Brewfile"
ok "Brewfile installed (~30 formulae + ~25 casks)"
else
warn "Brewfile not found alongside setup.sh — skipping brew bundle"
warn "Make sure you cloned the full gist: git clone https://gist.github.com/fad46568b34a09bf453fd48a57123252.git ~/m5-setup"
fi
# ============================================================
# 3. mise — pin language versions, activate inline
# ============================================================
step "3. Setting up mise (polyglot version manager)"
mise use --global node@lts python@3.13 rust@stable go@latest
# Activate mise in THIS shell so npm/python/etc are on PATH
eval "$(mise activate zsh)"
ok "mise active — node $(node --version 2>/dev/null), python $(python --version 2>/dev/null | awk '{print $2}')"
# ============================================================
# 4. npm globals (NOW node exists)
# ============================================================
step "4. Installing npm globals"
npm install -g @anthropic-ai/claude-code vercel pnpm 2>&1 | tail -5
ok "Claude Code + Vercel + pnpm installed"
# opencode — local agentic coding TUI (drives local Ollama/MLX models; see §22.5 / §23)
command -v opencode >/dev/null 2>&1 || curl -fsSL https://opencode.ai/install | bash
command -v opencode >/dev/null 2>&1 && ok "opencode installed (local Claude-Code-like TUI)"
# ============================================================
# 5. Python tools via uv (Solidity stack)
# ============================================================
step "5. Installing Solidity Python tools"
uv tool install slither-analyzer 2>&1 | tail -1
uv tool install halmos 2>&1 | tail -1
uv tool install eth-wake 2>&1 | tail -1 # PyPI name is eth-wake, NOT wake
ok "Slither + Halmos + Wake installed"
# ============================================================
# 6. Foundry (Ethereum toolkit — official curl installer)
# ============================================================
step "6. Installing Foundry"
if ! command -v foundryup &> /dev/null; then
curl -L https://foundry.paradigm.xyz | bash
export PATH="$HOME/.foundry/bin:$PATH"
foundryup
ok "Foundry installed (forge/cast/anvil/chisel)"
else
ok "Foundry already installed"
fi
# ============================================================
# 7. Write config files (with backup of existing)
# ============================================================
step "7. Writing config files"
# --- ~/.zshrc ---
backup_if_exists ~/.zshrc
cat > ~/.zshrc <<'ZSHRC'
# ============================================================
# ~/.zshrc — M5 Max (auto-generated by setup.sh)
# ============================================================
# ----- PATH -----
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:$PATH"
export PATH="$HOME/.local/bin:$PATH" # uv tools
export PATH="$HOME/.foundry/bin:$PATH" # Foundry
# ----- Antidote: zsh plugin manager -----
source $(brew --prefix)/opt/antidote/share/antidote/antidote.zsh
antidote load << 'EOF'
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-completions
zdharma-continuum/fast-syntax-highlighting
EOF
# ----- Prompt + interactive tools -----
eval "$(starship init zsh)"
eval "$(zoxide init zsh)"
eval "$(atuin init zsh)"
eval "$(mise activate zsh)"
eval "$(direnv hook zsh)"
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh
# ----- Claude Code (Ghostty paste workaround for issue #54700) -----
if [ "$TERM_PROGRAM" = "ghostty" ]; then
alias cc='TERM=xterm-256color claude'
else
alias cc='claude'
fi
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=15
export CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1
export CLAUDE_CODE_EFFORT_LEVEL=xhigh
export BASH_DEFAULT_TIMEOUT_MS=300000
# NOTE: do NOT set ANTHROPIC_BASE_URL — the `claude` CLI must stay on Anthropic cloud
# (Claude Max subscription). Local-model coding uses a SEPARATE tool (opencode / Cursor
# / Cline / Aider), wired to Ollama at localhost:11434 via that tool's own config — never
# by repointing claude. See walkthrough §22.5 for the local-AI lane setup.
# ----- HF token via 1Password (uncomment after 1P CLI setup) -----
# export HF_TOKEN="$(op item get 'HuggingFace' --field credential 2>/dev/null)"
# ----- Aliases -----
alias ls='eza --icons --git'
alias ll='eza -la --icons --git'
alias lt='eza --tree --level=2 --icons'
alias cat='bat --paging=never'
alias grep='rg'
alias find='fd'
alias top='btop'
alias du='dust'
alias df='duf'
alias g='git'
alias lg='lazygit'
alias gst='git status'
alias gco='git checkout'
alias gp='git push'
alias gpl='git pull'
alias dev='cd ~/1_PROJECTS'
alias bl='cd ~/projects/bounty-lab'
alias kb='cd ~/2_KNOWLEDGE'
# ----- MLX max-power functions -----
mlx_max() {
export MLX_METAL_FAST_SYNCH=1
defaults write NSGlobalDomain NSAppSleepDisabled -bool YES
caffeinate -di &
echo "MLX max: fast sync ON, sleep OFF, caffeinate"
}
mlx_revert() {
unset MLX_METAL_FAST_SYNCH
defaults delete NSGlobalDomain NSAppSleepDisabled
pkill caffeinate 2>/dev/null
echo "MLX max reverted"
}
# ----- fzf theme: Neon Noir -----
export FZF_DEFAULT_OPTS="
--color=bg+:#15151F,bg:#0A0A14,spinner:#FF006E,hl:#00D4FF
--color=fg:#F5F5F7,header:#00D4FF,info:#7C3AED,pointer:#FF006E
--color=marker:#34D399,fg+:#F5F5F7,prompt:#00D4FF,hl+:#FF006E
"
ZSHRC
ok "~/.zshrc written"
# --- ~/.gitconfig ---
backup_if_exists ~/.gitconfig
cat > ~/.gitconfig <<'GITCONFIG'
[user]
name = Adam Boudjemaa
email = boudjemaa.adam@gmail.com
signingkey = ~/.ssh/id_ed25519.pub
[gpg]
format = ssh
[gpg "ssh"]
program = /Applications/1Password.app/Contents/MacOS/op-ssh-sign
[commit]
gpgsign = true
[init]
defaultBranch = main
[pull]
rebase = false
[push]
autoSetupRemote = true
default = current
[fetch]
prune = true
[merge]
conflictstyle = zdiff3
[diff]
algorithm = histogram
colorMoved = default
[color]
ui = auto
[alias]
st = status
co = checkout
br = branch
cm = commit -m
lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit
last = log -1 HEAD
unstage = reset HEAD --
amend = commit --amend --no-edit
sw = switch
ll = log --oneline -20
GITCONFIG
ok "~/.gitconfig written (SSH commit signing via 1Password ready)"
# --- ~/.config/starship.toml ---
backup_if_exists ~/.config/starship.toml
cat > ~/.config/starship.toml <<'STARSHIP'
"$schema" = "https://starship.rs/config-schema.json"
format = """
[](#7C3AED)\
$os$username\
[](bg:#0A0A14 fg:#7C3AED)\
$directory\
[](fg:#0A0A14 bg:#0A0A14)\
$git_branch$git_status\
[](fg:#0A0A14 bg:#0A0A14)\
$nodejs$python$rust$golang\
[](fg:#0A0A14 bg:#15151F)\
$time\
[ ](fg:#15151F)\
$line_break$character"""
palette = "neon_noir"
[palettes.neon_noir]
cyan = "#00D4FF"
magenta = "#FF006E"
violet = "#7C3AED"
green = "#34D399"
dark = "#0A0A14"
surface = "#15151F"
[character]
success_symbol = "[❯](bold cyan)"
error_symbol = "[❯](bold magenta)"
[directory]
style = "bold cyan"
truncation_length = 3
truncate_to_repo = true
[git_branch]
style = "bold magenta"
symbol = " "
[git_status]
style = "bold magenta"
[time]
disabled = false
format = '[ $time ]($style)'
style = "fg:#71717A"
time_format = "%H:%M"
[cmd_duration]
min_time = 500
format = "[took $duration]($style) "
style = "fg:#FBBF24"
STARSHIP
ok "~/.config/starship.toml written"
# --- ~/.config/ghostty/config (Liquid Glass + Neon Noir) ---
backup_if_exists ~/.config/ghostty/config
cat > ~/.config/ghostty/config <<'GHOSTTY'
# Neon Noir — Liquid Glass on macOS Tahoe (26.x)
# Translucent design matching brand: #00D4FF / #FF006E / #7C3AED / #0A0A14
# === Theme (cyberdream = closest match to Neon Noir; falls back to tokyonight_storm if missing) ===
theme = cyberdream
# === Liquid Glass (macOS 26+) ===
background-opacity = 0.85
background-blur = macos-glass-regular
macos-titlebar-style = transparent
macos-icon = retro
# === Window ===
window-decoration = auto
window-padding-x = 14
window-padding-y = 12
window-padding-balance = true
window-vsync = true
window-save-state = always
window-colorspace = display-p3
# === Font (install JetBrainsMono Nerd Font first: brew install --cask font-jetbrains-mono-nerd-font) ===
font-family = JetBrainsMono Nerd Font
font-size = 14
font-thicken = true
font-feature = +liga
font-feature = +calt
font-feature = +ss01
font-feature = +zero
adjust-cell-height = 15%
# === Cursor + mouse ===
cursor-style = bar
cursor-style-blink = true
mouse-hide-while-typing = true
# === Shell + behavior ===
shell-integration = detect
shell-integration-features = cursor,sudo,title
copy-on-select = clipboard
clipboard-read = allow
clipboard-write = allow
clipboard-paste-protection = true
confirm-close-surface = false
unfocused-split-opacity = 0.85
# === Neon Noir accents (paint cyan/magenta/violet OVER cyberdream) ===
cursor-color = #00D4FF
selection-background = #7C3AED
selection-foreground = #FFFFFF
# === Keybinds ===
keybind = cmd+t=new_tab
keybind = cmd+d=new_split:right
keybind = cmd+shift+d=new_split:down
keybind = cmd+w=close_surface
keybind = cmd+shift+enter=toggle_split_zoom
keybind = cmd+enter=toggle_fullscreen
# === v2 polish (Scout A, 2026-05-27) — 10 verified items, all sourced to ghostty.org/docs ===
# Quick Terminal — Quake-style drop-down via cmd+` (global hotkey)
keybind = global:cmd+grave_accent=toggle_quick_terminal
quick-terminal-position = top
quick-terminal-animation-duration = 0.18
quick-terminal-screen = main
quick-terminal-autohide = true
# Command Palette (cmd+shift+p) — VS Code muscle memory for in-app actions
keybind = cmd+shift+p=toggle_command_palette
# Dynamic tab title: CWD basename + git branch (re-runs via shell integration)
title-command = printf '%s %s' "$(basename "$PWD")" "$(git branch --show-current 2>/dev/null)"
# Bell: visual + soft audio, no dock-bounce (1.3.0)
bell-features = visual,audio,no-system
bell-audio-volume = 0.4
# Notify on long commands when unfocused (1.3.0)
notify-on-command-finish = unfocused
notify-on-command-finish-action = notify
notify-on-command-finish-after = 10s
# Granular working-directory inheritance (1.3.0)
window-inherit-working-directory = true
tab-inherit-working-directory = true
split-inherit-working-directory = true
# Native Tahoe overlay scrollbar (1.3.0)
scrollbar = system
# Left option = Alt (unlocks tmux/helix); right option stays free for français accents
macos-option-as-alt = left
# Brand-color unfocused-split fill (overlays #0A0A14 instead of alpha hack)
unfocused-split-fill = #0A0A14
# Resize/padding/blending polish trio
resize-overlay = after-first
window-padding-color = extend
alpha-blending = native
GHOSTTY
ok "~/.config/ghostty/config written (Liquid Glass + Neon Noir)"
# Add Neon Noir custom theme (fallback if cyberdream not bundled)
mkdir -p ~/.config/ghostty/themes
cat > ~/.config/ghostty/themes/neon-noir <<'NEONNOIR'
background = 0A0A14
foreground = E0E0FF
cursor-color = 00D4FF
selection-background = 7C3AED
selection-foreground = FFFFFF
palette = 0=#0A0A14
palette = 1=#FF006E
palette = 2=#34D399
palette = 3=#FBBF24
palette = 4=#7C3AED
palette = 5=#FF006E
palette = 6=#00D4FF
palette = 7=#F5F5F7
palette = 8=#71717A
palette = 9=#FF4D8F
palette = 10=#6EE7B7
palette = 11=#FCD34D
palette = 12=#A78BFA
palette = 13=#FF6BA8
palette = 14=#7DF0FF
palette = 15=#FFFFFF
NEONNOIR
ok "~/.config/ghostty/themes/neon-noir written (custom palette fallback)"
# Install JetBrains Mono Nerd Font if not already installed
if ! brew list --cask font-jetbrains-mono-nerd-font &> /dev/null; then
brew install --cask font-jetbrains-mono-nerd-font 2>&1 | tail -1
ok "JetBrainsMono Nerd Font installed"
else
ok "JetBrainsMono Nerd Font already installed"
fi
# --- ~/.config/mise/config.toml ---
backup_if_exists ~/.config/mise/config.toml
cat > ~/.config/mise/config.toml <<'MISE'
[tools]
node = "lts"
python = "3.13"
rust = "stable"
go = "latest"
[settings]
experimental = true
trusted_config_paths = ["~/projects", "~/1_PROJECTS"]
MISE
ok "~/.config/mise/config.toml written"
# --- ~/.config/atuin/config.toml ---
backup_if_exists ~/.config/atuin/config.toml
cat > ~/.config/atuin/config.toml <<'ATUIN'
enter_accept = false
filter_mode = "global"
filter_mode_shell_up_key_arrow = "session"
style = "compact"
inline_height = 20
show_preview = true
show_help = true
auto_sync = true
sync_frequency = "10m"
[stats]
common_prefix = ["sudo", "time"]
ATUIN
ok "~/.config/atuin/config.toml written"
# --- ~/.claude/settings.json ---
backup_if_exists ~/.claude/settings.json
cat > ~/.claude/settings.json <<'CLAUDE'
{
"model": "claude-opus-4-7",
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "15",
"CLAUDE_CODE_EFFORT_LEVEL": "xhigh",
"CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
},
"permissions": {
"deny": [
"Bash(rm -rf /*)",
"Bash(cat /.env*)",
"Bash(cat ~/.ssh/*)",
"Bash(cat ~/.aws/*)"
],
"ask": [
"Bash(git push --force*)",
"Bash(rm -rf*)",
"Bash(npm publish*)"
]
},
"cleanupPeriodDays": 30
}
CLAUDE
ok "~/.claude/settings.json written"
# --- ~/.ssh/config (with Keychain integration) ---
mkdir -p ~/.ssh
chmod 700 ~/.ssh
if ! grep -q "Host github.com" ~/.ssh/config 2>/dev/null; then
cat >> ~/.ssh/config <<'SSHCONFIG'
Host github.com
HostName github.com
User git
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
SSHCONFIG
chmod 600 ~/.ssh/config
fi
ok "~/.ssh/config written (Keychain integration)"
# ============================================================
# 8. SSH key gen + upload to GitHub via gh CLI
# ============================================================
step "8. SSH key + GitHub upload"
if [ ! -f ~/.ssh/id_ed25519 ]; then
ssh-keygen -t ed25519 -C "boudjemaa.adam@gmail.com" -f ~/.ssh/id_ed25519 -N ""
ok "SSH key generated at ~/.ssh/id_ed25519"
else
ok "SSH key already exists"
fi
# Add to SSH agent + Keychain
ssh-add --apple-use-keychain ~/.ssh/id_ed25519 2>/dev/null || true
ok "SSH key added to agent + Keychain"
# Check gh CLI auth status
if ! gh auth status &> /dev/null; then
warn "gh CLI not authenticated. Opening browser for login..."
warn "Choose: GitHub.com → HTTPS → Login with browser"
gh auth login --web --hostname github.com --git-protocol https
fi
if gh auth status &> /dev/null; then
# Upload as auth key
if ! gh ssh-key list 2>/dev/null | grep -q "M5 Max auth"; then
gh ssh-key add ~/.ssh/id_ed25519.pub --title "Adam M5 Max auth"
ok "SSH key uploaded as auth key"
fi
# Upload as signing key
if ! gh ssh-key list --type signing 2>/dev/null | grep -q "M5 Max signing"; then
gh ssh-key add ~/.ssh/id_ed25519.pub --type signing --title "Adam M5 Max signing"
ok "SSH key uploaded as signing key"
fi
# Verify auth works
if ssh -T git@github.com 2>&1 | grep -q "successfully authenticated"; then
ok "GitHub SSH authentication verified"
else
warn "GitHub SSH verification failed. Try: ssh -T git@github.com"
fi
else
warn "gh CLI auth skipped — run later: gh auth login && gh ssh-key add ~/.ssh/id_ed25519.pub"
fi
# ============================================================
# 9. macOS defaults
# ============================================================
step "9. macOS defaults (Finder, dock, key repeat, screenshots)"
# Finder
defaults write com.apple.finder AppleShowAllFiles -bool true
defaults write com.apple.finder _FXShowPosixPathInTitle -bool true
defaults write com.apple.finder ShowPathbar -bool true
defaults write com.apple.finder ShowStatusBar -bool true
# Key repeat
defaults write -g KeyRepeat -int 2
defaults write -g InitialKeyRepeat -int 15
# Kill smart quotes/dashes/spell
defaults write -g NSAutomaticDashSubstitutionEnabled -bool false
defaults write -g NSAutomaticQuoteSubstitutionEnabled -bool false
defaults write -g NSAutomaticSpellingCorrectionEnabled -bool false
# Dock
defaults write com.apple.dock autohide -bool true
defaults write com.apple.dock autohide-delay -float 0
defaults write com.apple.dock autohide-time-modifier -float 0.3
defaults write com.apple.dock show-recents -bool false
# Screenshots → ~/Pictures/Screenshots
defaults write com.apple.screencapture location ~/Pictures/Screenshots
ok "macOS defaults applied"
# ============================================================
# 10. Disable Spotlight ⌘+Space (so Raycast can take it)
# ============================================================
step "10. Disabling Spotlight ⌘+Space hotkey (for Raycast)"
defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 64 \
'<dict><key>enabled</key><false/></dict>' 2>/dev/null || true
defaults write com.apple.symbolichotkeys AppleSymbolicHotKeys -dict-add 65 \
'<dict><key>enabled</key><false/></dict>' 2>/dev/null || true
ok "Spotlight ⌘+Space disabled (Raycast can claim it after first launch)"
# Apply defaults
killall Finder Dock SystemUIServer 2>/dev/null || true
# ============================================================
# 11. Ollama service + base model pulls (background)
# ============================================================
step "11. Starting Ollama + pulling base models (background)"
brew services start ollama 2>&1 | tail -1
sleep 3
# Ollama tuning (M5): keep models warm + large context. Ollama's tiny default context
# silently truncates long prompts → broken tool calls; raise it to 16384. Plus flash-attn
# and q8 KV cache. Persisted in the brew LaunchAgent plist's EnvironmentVariables.
OLLAMA_PLIST="$HOME/Library/LaunchAgents/homebrew.mxcl.ollama.plist"
if [ -f "$OLLAMA_PLIST" ]; then
/usr/libexec/PlistBuddy -c "Add :EnvironmentVariables dict" "$OLLAMA_PLIST" 2>/dev/null || true
for kv in OLLAMA_FLASH_ATTENTION=1 OLLAMA_KV_CACHE_TYPE=q8_0 OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=16384; do
k="${kv%%=*}"; v="${kv#*=}"
/usr/libexec/PlistBuddy -c "Set :EnvironmentVariables:$k $v" "$OLLAMA_PLIST" 2>/dev/null \
|| /usr/libexec/PlistBuddy -c "Add :EnvironmentVariables:$k string $v" "$OLLAMA_PLIST"
done
launchctl bootout "gui/$(id -u)/homebrew.mxcl.ollama" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$OLLAMA_PLIST" 2>/dev/null || true
ok "Ollama env tuned (KEEP_ALIVE=-1, CONTEXT_LENGTH=16384, flash-attn, q8 KV cache)"
fi
# Pull in background — don't block setup.
# Uncensored/abliterated equivalents of the qwen3 set (Adam's intentional choice).
(
ollama pull huihui_ai/qwen3-abliterated:30b # daily driver text (≈ qwen3:30b)
ollama pull huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0 # coding base (≈ qwen3-coder:30b)
ollama pull nomic-embed-text # RAG embeddings
ollama pull huihui_ai/qwen3.5-abliterated:122B # flagship reasoning (~81 GB)
) &
OLLAMA_PID=$!
ok "Ollama pulls running in background (PID $OLLAMA_PID) — ~131 GB total"
# ============================================================
# 12. Whisper model (with truncation guard)
# ============================================================
step "12. Downloading Whisper model"
mkdir -p ~/.cache/whisper-cpp
if [ ! -f ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin ]; then
curl -L --fail --retry 3 -o ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin?download=true" \
2>&1 | tail -1
size=$(stat -f%z ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin)
if [ "$size" -lt 1500000000 ]; then
warn "Whisper model truncated ($size bytes, expected ~1.6 GB). Removing — re-run setup.sh"
rm ~/.cache/whisper-cpp/ggml-large-v3-turbo.bin
else
ok "Whisper model downloaded ($(echo "scale=2; $size/1073741824" | bc) GB)"
fi
else
ok "Whisper model already downloaded"
fi
# ============================================================
# 13. GPU memory cap (sysctl live + LaunchDaemon for reboot persistence)
# ============================================================
# Modern macOS (Tahoe 26.x) does NOT auto-load /etc/sysctl.conf at boot.
# The reliable persistence path is a LaunchDaemon that runs `sysctl` on every boot.
step "13. Setting GPU memory cap to 120 GB — sysctl live + LaunchDaemon persistence (needs sudo)"
GPU_PLIST=/Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist
if [ "$(sysctl -n iogpu.wired_limit_mb 2>/dev/null)" != "122880" ]; then
sudo sysctl iogpu.wired_limit_mb=122880
ok "GPU cap set live to 122880 MB (120 GB)"
else
ok "GPU cap already at 122880 MB"
fi
if [ ! -f "$GPU_PLIST" ]; then
sudo tee "$GPU_PLIST" > /dev/null <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.adam.iogpu-wired-limit</string>
<key>ProgramArguments</key>
<array>
<string>/usr/sbin/sysctl</string>
<string>iogpu.wired_limit_mb=122880</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<false/>
</dict>
</plist>
PLIST
sudo chown root:wheel "$GPU_PLIST"
sudo chmod 644 "$GPU_PLIST"
sudo launchctl load -w "$GPU_PLIST" 2>/dev/null || true
ok "GPU cap persisted via LaunchDaemon ($GPU_PLIST)"
else
ok "GPU cap LaunchDaemon already present"
fi
# High Power Mode (AC profile) — sustains GPU clocks under long MLX loads. Engages on AC.
if [ "$(pmset -g custom 2>/dev/null | awk '/AC Power/{f=1} f&&/powermode/{print $2; exit}')" != "2" ]; then
sudo pmset -c powermode 2 && ok "High Power Mode enabled for AC (powermode 2)"
else
ok "High Power Mode already enabled for AC (powermode 2)"
fi
# Touch ID for sudo (scriptable) — Touch-ID-backed sudo via the system template
if [ ! -f /etc/pam.d/sudo_local ] && [ -f /etc/pam.d/sudo_local.template ]; then
sudo sed 's/^#auth/auth/' /etc/pam.d/sudo_local.template | sudo tee /etc/pam.d/sudo_local >/dev/null
ok "Touch ID for sudo enabled (/etc/pam.d/sudo_local)"
fi
# ============================================================
# 14. MLX project scaffold
# ============================================================
step "14. Scaffolding MLX lab project"
if [ ! -d ~/1_PROJECTS/mlx-lab ]; then
mkdir -p ~/1_PROJECTS/mlx-lab
cd ~/1_PROJECTS/mlx-lab
uv init -q --no-readme 2>/dev/null
uv add mlx mlx-lm mlx-lm-lora mlx-vlm mlx-audio mlx-embeddings mflux 2>&1 | tail -3
cd - > /dev/null
ok "MLX lab venv created at ~/1_PROJECTS/mlx-lab"
else
ok "MLX lab already exists"
fi
# ============================================================
# 15. fzf keybindings
# ============================================================
step "15. Wiring fzf keybindings"
yes | $(brew --prefix)/opt/fzf/install --all --no-update-rc 2>/dev/null > /dev/null || true
ok "fzf keybindings ready (Ctrl+R, Ctrl+T, Alt+C)"
# ============================================================
# DONE
# ============================================================
echo ""
echo -e "${C_GREEN}═════════════════════════════════════════════════════${C_RESET}"
echo -e "${C_GREEN} ✅ Automated setup complete${C_RESET}"
echo -e "${C_GREEN}═════════════════════════════════════════════════════${C_RESET}"
echo ""
echo "Manual steps remaining (cannot be automated — UI logins / licenses):"
echo ""
echo -e " ${C_BLUE}1.${C_RESET} Restart your shell to load the new ~/.zshrc:"
echo " exec zsh"
echo ""
echo -e " ${C_BLUE}2.${C_RESET} Sign in to GUI apps:"
echo " • Cursor (sign in via UI)"
echo " • Claude Code → run 'claude' for OAuth"
echo " • 1Password (sign in to your account)"
echo " • Notion / Slack / Zoom / etc."
echo ""
echo -e " ${C_BLUE}3.${C_RESET} Configure Raycast:"
echo " • Open Raycast → Settings → General → Hotkey: ⌥+Space"
echo " • (Spotlight ⌘+Space already disabled by this script)"
echo ""
echo -e " ${C_BLUE}4.${C_RESET} Licenses to activate (paid apps):"
echo " • Hazel (\$42) — open Hazel, paste license"
echo " • CleanShot X (\$30) — sign in to setapp/setapp.com or buy license"
echo " • Cold Turkey Pro (\$39) — sign in"
echo ""
echo -e " ${C_BLUE}5.${C_RESET} Wait for Ollama pulls to finish (~80 GB) — check progress:"
echo " ollama list"
echo ""
echo -e " ${C_BLUE}6.${C_RESET} Smoke tests:"
echo " python -c \"import sys; sys.path.insert(0,'\$HOME/1_PROJECTS/mlx-lab/.venv/lib/python3.13/site-packages'); import mlx.core as mx; print('MLX Metal:', mx.metal.is_available())\""
echo " ssh -T git@github.com"
echo " forge --version"
echo " cc --bare -p 'hello'"
echo ""
echo "Reference docs: open adam-mac-setup.md in this gist for full explanations."
echo ""
{
"cleanupPeriodDays": 30,
"env": {
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY": "32",
"CLAUDE_CODE_EFFORT_LEVEL": "xhigh",
"CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1"
},
"permissions": {
"allow": [
"Bash",
"Read",
"Edit",
"Write",
"Grep",
"Glob"
],
"deny": [
"Bash(rm -rf /*)",
"Bash(cat /.env*)",
"Bash(cat ~/.ssh/*)",
"Bash(cat ~/.aws/*)"
],
"ask": [
"Bash(git push --force*)",
"Bash(rm -rf*)",
"Bash(npm publish*)"
]
},
"model": "opus",
"effortLevel": "xhigh",
"skipDangerousModePermissionPrompt": true,
"skipWorkflowUsageWarning": true,
"theme": "dark",
"fileCheckpointingEnabled": true,
"disableDeepLinkRegistration": "disable",
"enabledPlugins": {
"swift-lsp@claude-plugins-official": true
}
}
{
"[solidity]": {
"editor.defaultFormatter": "NomicFoundation.hardhat-solidity",
"editor.formatOnSave": true
},
"solidity.formatter": "forge",
"solidity.packageDefaultDependenciesContractsDirectory": "src",
"solidity.packageDefaultDependenciesDirectory": "lib",
"todo-tree.ripgrep.ripgrep": "/opt/homebrew/bin/rg",
"security.workspace.trust.untrustedFiles": "open"
}
# === Claude Code: ultracode + bypass-perms by default, Ghostty paste fix (Anthropic #54700) ===
# Defaults: --settings='{"ultracode":true}' forces ultracode (settings.json drops it silently),
# --dangerously-skip-permissions bypasses the per-tool ask/allow prompts.
# Escape hatches: use `command claude` or `\claude` to launch without the defaults.
cc() {
if [ "$TERM_PROGRAM" = "ghostty" ]; then
TERM=xterm-256color command claude --settings='{"ultracode":true}' --dangerously-skip-permissions "$@"
else
command claude --settings='{"ultracode":true}' --dangerously-skip-permissions "$@"
fi
}
# Same defaults applied when typing `claude` directly
claude() {
command claude --settings='{"ultracode":true}' --dangerously-skip-permissions "$@"
}
#!/usr/bin/env zsh
# ============================================================
# Adam M5 Max — verification script
#
# Usage: ./verify.sh
#
# Checks:
# 1. Foundation (Xcode, Homebrew, paths)
# 2. Every Brewfile formula/cask is installed → reports missing
# 3. Dropped/redundant items still installed → offers to uninstall
# 4. Dev runtimes via mise (node/python/rust/go)
# 5. Python tools via uv (slither, halmos, eth-wake)
# 6. Foundry (forge/cast/anvil/chisel)
# 7. npm globals (Claude Code, vercel, pnpm)
# 8. Ollama service + base models
# 9. Whisper model file + size
# 10. SSH key + GitHub auth
# 11. macOS defaults applied
# 12. sysctl GPU cap set
# 13. Config files exist (.zshrc, .gitconfig, .config/*, .claude/settings.json)
# 14. Claude Code env vars present in shell
# 15. MLX project venv
#
# Exit 0 = all green. Non-zero = issues found (counted at bottom).
# ============================================================
C_GREEN='\033[0;32m'
C_BLUE='\033[0;34m'
C_YELLOW='\033[0;33m'
C_RED='\033[0;31m'
C_GRAY='\033[0;90m'
C_RESET='\033[0m'
PASS=0
FAIL=0
WARN=0
TO_INSTALL=()
TO_REMOVE=()
ok() { echo -e " ${C_GREEN}✓${C_RESET} $1"; PASS=$((PASS+1)); }
miss() { echo -e " ${C_RED}✗${C_RESET} $1"; FAIL=$((FAIL+1)); }
warn() { echo -e " ${C_YELLOW}⚠${C_RESET} $1"; WARN=$((WARN+1)); }
info() { echo -e " ${C_GRAY}·${C_RESET} $1"; }
section() { echo -e "\n${C_BLUE}▶ $1${C_RESET}"; } # renamed from head() — was shadowing /usr/bin/head and breaking line 190
# ============================================================
# 1. Foundation
# ============================================================
section "1. Foundation"
xcode-select -p &>/dev/null && ok "Xcode CLI tools" || { miss "Xcode CLI tools — run: xcode-select --install"; }
command -v brew &>/dev/null && ok "Homebrew at $(which brew)" || miss "Homebrew not installed"
[[ "$(which brew)" == "/opt/homebrew/bin/brew" ]] && ok "Homebrew on Apple Silicon path" || warn "Brew not at /opt/homebrew (Intel/Rosetta?)"
[[ "$(uname -m)" == "arm64" ]] && ok "Apple Silicon arch" || warn "Not Apple Silicon"
# ============================================================
# 2. Required formulae (Brewfile spec)
# ============================================================
section "2. Required brew formulae"
REQUIRED_FORMULAE=(
antidote starship atuin
eza bat fd ripgrep fzf zoxide
lazygit yazi gh jq yq tree
htop btop dust duf
direnv tldr gnupg
mkcert mas xh doggo
wget watch imagemagick yt-dlp ncdu
mise uv git
ollama whisper-cpp ffmpeg llama.cpp
echidna solana aptos
awscli doctl cloudflared
)
INSTALLED_FORMULAE=$(brew list --formula 2>/dev/null)
for f in "${REQUIRED_FORMULAE[@]}"; do
if echo "$INSTALLED_FORMULAE" | grep -qx "$f"; then
ok "formula $f"
else
miss "formula $f"
TO_INSTALL+=("brew install $f")
fi
done
# ============================================================
# 3. Required casks
# ============================================================
section "3. Required brew casks"
REQUIRED_CASKS=(
ghostty visual-studio-code
brave-browser google-chrome firefox
raycast jordanbaird-ice obsidian
tailscale-app
lm-studio draw-things comfyui
orbstack
cleanshot
1password 1password-cli
postman
whatsapp telegram zoom
alt-tab stats appcleaner the-unarchiver
iina voiceink
font-jetbrains-mono-nerd-font
)
INSTALLED_CASKS=$(brew list --cask 2>/dev/null)
for c in "${REQUIRED_CASKS[@]}"; do
if echo "$INSTALLED_CASKS" | grep -qx "$c"; then
ok "cask $c"
else
miss "cask $c"
TO_INSTALL+=("brew install --cask $c")
fi
done
# Tap formulae
echo "$INSTALLED_FORMULAE" | grep -q "^aderyn$" && ok "formula aderyn (cyfrin/tap)" || { miss "aderyn (needs cyfrin/tap)"; TO_INSTALL+=("brew install cyfrin/tap/aderyn"); }
echo "$INSTALLED_FORMULAE" | grep -q "^supabase$" && ok "formula supabase (core)" || { miss "supabase CLI"; TO_INSTALL+=("brew install supabase"); }
echo "$INSTALLED_FORMULAE" | grep -q "^stripe$" && ok "formula stripe (tap)" || { miss "stripe CLI"; TO_INSTALL+=("brew install stripe/stripe-cli/stripe"); }
echo "$INSTALLED_FORMULAE" | grep -q "^sui$" && ok "formula sui (core)" || { miss "sui CLI"; TO_INSTALL+=("brew install sui"); }
# ============================================================
# 4. Dropped/redundant items (offer to uninstall)
# ============================================================
section "4. Dropped items still installed (cleanup candidates)"
DROPPED_FORMULAE=(procs bottom httpie)
DROPPED_CASKS=(mstystudio msty rectangle sleeve wezterm spotify zen-browser pinokio notion notion-calendar slack signal)
for f in "${DROPPED_FORMULAE[@]}"; do
if echo "$INSTALLED_FORMULAE" | grep -qx "$f"; then
warn "formula $f installed — dropped in latest spec (redundant)"
TO_REMOVE+=("brew uninstall $f")
fi
done
for c in "${DROPPED_CASKS[@]}"; do
if echo "$INSTALLED_CASKS" | grep -qx "$c"; then
warn "cask $c installed — dropped in latest spec"
TO_REMOVE+=("brew uninstall --cask $c")
fi
done
# Spotify reframe
if echo "$INSTALLED_CASKS" | grep -qx "spotify"; then
warn "spotify installed — you use Apple Music. If true, uninstall: brew uninstall --cask spotify"
fi
# Docker Desktop check (we want OrbStack instead)
if echo "$INSTALLED_CASKS" | grep -qx "docker"; then
warn "Docker Desktop installed — OrbStack recommended instead (10× lighter)"
TO_REMOVE+=("brew uninstall --cask docker")
fi
# Cursor check (switched to VS Code for unrestricted local-LLM support)
if echo "$INSTALLED_CASKS" | grep -qx "cursor"; then
warn "Cursor installed — VS Code is the chosen editor (Continue.dev wires local Ollama/MLX)"
TO_REMOVE+=("brew uninstall --cask cursor")
fi
# Zen browser if Brave is primary
if echo "$INSTALLED_CASKS" | grep -qx "zen" && echo "$INSTALLED_CASKS" | grep -qx "brave-browser"; then
info "Both Brave + Zen installed — fine if you use both. If not: brew uninstall --cask zen"
fi
# ============================================================
# 5. Dev runtimes (via mise)
# ============================================================
section "5. Dev runtimes (mise)"
if command -v mise &>/dev/null; then
CURRENT=$(mise current 2>/dev/null)
# `mise current` prints "<tool> <version>" — 2 fields, version is $2 (NOT $3 — that bug returned empty since mise 2025.x)
echo "$CURRENT" | grep -q "node" && ok "node ($(echo "$CURRENT" | grep node | awk '{print $2}'))" || miss "node not pinned via mise"
echo "$CURRENT" | grep -q "python" && ok "python ($(echo "$CURRENT" | grep python | awk '{print $2}'))" || miss "python not pinned via mise"
echo "$CURRENT" | grep -q "rust" && ok "rust ($(echo "$CURRENT" | grep rust | awk '{print $2}'))" || miss "rust not pinned via mise"
echo "$CURRENT" | grep -q "go" && ok "go ($(echo "$CURRENT" | grep go | awk '{print $2}'))" || miss "go not pinned via mise"
else
miss "mise not installed"
fi
# ============================================================
# 6. Python tools (uv)
# ============================================================
section "6. Python tools (via uv)"
UV_TOOLS=$(uv tool list 2>/dev/null)
for t in slither-analyzer halmos eth-wake; do
echo "$UV_TOOLS" | grep -q "^$t " && ok "uv tool $t" || { miss "uv tool $t"; TO_INSTALL+=("uv tool install $t"); }
done
# ============================================================
# 7. Foundry
# ============================================================
section "7. Foundry"
command -v forge &>/dev/null && ok "forge ($(forge --version 2>/dev/null | head -1))" || { miss "forge — run: curl -L https://foundry.paradigm.xyz | bash && foundryup"; }
command -v cast &>/dev/null && ok "cast" || miss "cast"
command -v anvil &>/dev/null && ok "anvil" || miss "anvil"
# ============================================================
# 8. npm globals
# ============================================================
section "8. npm globals"
NPM_GLOBALS=$(npm list -g --depth=0 2>/dev/null || echo "")
echo "$NPM_GLOBALS" | grep -q "@anthropic-ai/claude-code" && ok "claude-code" || { miss "claude-code"; TO_INSTALL+=("npm install -g @anthropic-ai/claude-code"); }
echo "$NPM_GLOBALS" | grep -q "vercel" && ok "vercel" || { miss "vercel"; TO_INSTALL+=("npm install -g vercel"); }
echo "$NPM_GLOBALS" | grep -q "pnpm" && ok "pnpm" || { miss "pnpm"; TO_INSTALL+=("npm install -g pnpm"); }
# ============================================================
# 9. Ollama service + models
# ============================================================
section "9. Ollama"
if pgrep -x ollama &>/dev/null || brew services list 2>/dev/null | grep -q "ollama.*started"; then
ok "Ollama running"
else
miss "Ollama not running — run: brew services start ollama"
fi
OLLAMA_MODELS=$(ollama list 2>/dev/null | tail -n +2 | awk '{print $1}')
# Adam runs the uncensored/abliterated equivalents of the qwen3 set (intentional, NOT drift):
# qwen3:30b → huihui_ai/qwen3-abliterated:30b · qwen3-coder:30b → huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0
# Bonus flagship (not required here): huihui_ai/qwen3.5-abliterated:122B
for m in "huihui_ai/qwen3-abliterated:30b" "huihui_ai/qwen3-coder-abliterated:30b-a3b-instruct-q8_0" "nomic-embed-text"; do
# Match both bare name AND :latest tag; retry once on transient miss
if echo "$OLLAMA_MODELS" | grep -qE "^${m}(:latest)?$"; then
ok "ollama model $m"
else
sleep 1
OLLAMA_MODELS=$(ollama list 2>/dev/null | tail -n +2 | awk '{print $1}')
if echo "$OLLAMA_MODELS" | grep -qE "^${m}(:latest)?$"; then
ok "ollama model $m"
else
miss "ollama model $m"; TO_INSTALL+=("ollama pull $m")
fi
fi
done
# ============================================================
# 10. Whisper model
# ============================================================
section "10. Whisper model"
WHISPER_BIN=~/.cache/whisper-cpp/ggml-large-v3-turbo.bin
if [[ -f "$WHISPER_BIN" ]]; then
size=$(stat -f%z "$WHISPER_BIN" 2>/dev/null || echo 0)
if [[ "$size" -gt 1500000000 ]]; then
ok "whisper model present ($(echo "scale=2;$size/1073741824" | bc) GB)"
else
warn "whisper model truncated ($size bytes) — re-download"
fi
else
miss "whisper model missing — see setup.sh step 12"
fi
# ============================================================
# 11. SSH + GitHub
# ============================================================
section "11. SSH + GitHub"
[[ -f ~/.ssh/id_ed25519 ]] && ok "SSH key exists" || miss "SSH key missing — run: ssh-keygen -t ed25519 -C \"$(git config user.email)\""
[[ -f ~/.ssh/config ]] && grep -q "Host github.com" ~/.ssh/config && ok "~/.ssh/config has github" || warn "~/.ssh/config missing github entry"
if gh auth status &>/dev/null; then
ok "gh CLI authenticated"
else
warn "gh CLI not authenticated — run: gh auth login"
fi
if ssh -o BatchMode=yes -o ConnectTimeout=5 -T git@github.com 2>&1 | grep -q "successfully authenticated"; then
ok "GitHub SSH auth working"
else
miss "GitHub SSH auth failing — see Step 7 of gist"
fi
# Git commit signing (SSH-backed via 1Password — spec'd in setup.sh §7)
GIT_SIGNINGKEY=$(git config --global --get user.signingkey 2>/dev/null)
GIT_GPGFORMAT=$(git config --global --get gpg.format 2>/dev/null)
GIT_GPGSIGN=$(git config --global --get commit.gpgsign 2>/dev/null)
if [[ -n "$GIT_SIGNINGKEY" && "$GIT_GPGFORMAT" == "ssh" && "$GIT_GPGSIGN" == "true" ]]; then
ok "git commit signing configured (SSH, signingkey=$GIT_SIGNINGKEY)"
else
warn "git commit signing not fully configured (signingkey='$GIT_SIGNINGKEY' format='$GIT_GPGFORMAT' sign='$GIT_GPGSIGN') — see setup.sh §7"
fi
# ============================================================
# 12. macOS defaults
# ============================================================
section "12. macOS defaults"
[[ "$(defaults read com.apple.finder AppleShowAllFiles 2>/dev/null)" == "1" ]] && ok "Finder shows hidden files" || warn "Finder hidden files OFF"
[[ "$(defaults read -g KeyRepeat 2>/dev/null)" == "2" ]] && ok "Key repeat = max" || warn "Key repeat not maxed"
[[ "$(defaults read com.apple.dock autohide 2>/dev/null)" == "1" ]] && ok "Dock auto-hide" || warn "Dock not auto-hiding"
[[ "$(defaults read -g NSAutomaticDashSubstitutionEnabled 2>/dev/null)" == "0" ]] && ok "Smart dashes OFF" || warn "Smart dashes ON (breaks code paste)"
# ============================================================
# 13. sysctl GPU cap (M5 Max)
# ============================================================
section "13. M5 GPU memory cap"
CURRENT_CAP=$(sysctl -n iogpu.wired_limit_mb 2>/dev/null || echo 0)
if [[ "$CURRENT_CAP" == "122880" ]]; then
ok "iogpu.wired_limit_mb = 122880 (120 GB)"
else
miss "iogpu.wired_limit_mb = $CURRENT_CAP (expected 122880) — run: sudo sysctl iogpu.wired_limit_mb=122880"
fi
# Modern macOS (Tahoe 26.x) doesn't auto-load /etc/sysctl.conf at boot. LaunchDaemon is canonical.
GPU_PLIST=/Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist
if [[ -f "$GPU_PLIST" ]]; then
ok "GPU cap persisted via LaunchDaemon ($GPU_PLIST)"
elif grep -q "iogpu.wired_limit_mb" /etc/sysctl.conf 2>/dev/null; then
ok "GPU cap persisted in /etc/sysctl.conf (legacy path — LaunchDaemon preferred)"
else
warn "GPU cap not persisted (no LaunchDaemon, no /etc/sysctl.conf entry) — re-applies after reboot"
fi
# ============================================================
# 14. Config files
# ============================================================
section "14. Config files"
CONFIGS=(
"$HOME/.zshrc"
"$HOME/.gitconfig"
"$HOME/.config/starship.toml"
"$HOME/.config/ghostty/config"
"$HOME/.config/atuin/config.toml"
"$HOME/.config/mise/config.toml"
"$HOME/.claude/settings.json"
)
for f in "${CONFIGS[@]}"; do
[[ -f "$f" ]] && ok "$f" || miss "$f"
done
# ============================================================
# 15. Claude Code env in shell
# ============================================================
section "15. Claude Code env vars in ~/.zshrc"
grep -q "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC" ~/.zshrc 2>/dev/null && ok "DISABLE_NONESSENTIAL_TRAFFIC set" || warn "missing CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC in ~/.zshrc"
grep -q "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY" ~/.zshrc 2>/dev/null && ok "MAX_TOOL_USE_CONCURRENCY set" || warn "missing CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY"
grep -q "TERM=xterm-256color claude" ~/.zshrc 2>/dev/null && ok "Ghostty paste workaround alias 'cc' present" || warn "missing Ghostty 'cc' alias"
# Subscription guardrail — `claude` CLI MUST NOT be routed to a local endpoint.
# Setting ANTHROPIC_BASE_URL hijacks the Claude Max subscription endpoint and silently
# swaps the cloud model for a local Qwen. This check fails LOUDLY if it's set anywhere.
if grep -qE "^[[:space:]]*export[[:space:]]+ANTHROPIC_BASE_URL=" ~/.zshrc 2>/dev/null; then
miss "ANTHROPIC_BASE_URL exported in ~/.zshrc — claude CLI is hijacked to local. UNSET it."
else
ok "ANTHROPIC_BASE_URL not in ~/.zshrc (claude stays on Claude Max cloud)"
fi
if [ -n "$ANTHROPIC_BASE_URL" ]; then
miss "ANTHROPIC_BASE_URL=$ANTHROPIC_BASE_URL is currently set in shell — unset before running claude"
fi
# Ghostty v2 polish marker (setup.sh §21 writes it; Prompt 3 reapplies if missing)
if [[ -f ~/.config/ghostty/config ]] && grep -q "v2 polish" ~/.config/ghostty/config 2>/dev/null; then
ok "Ghostty v2 polish marker present"
else
warn "Ghostty v2 polish missing — run Prompt 3 to apply (no-op if already present)"
fi
# Neon Noir custom palette theme file (setup.sh §21.4b)
[[ -f ~/.config/ghostty/themes/neon-noir ]] && ok "~/.config/ghostty/themes/neon-noir" || warn "Neon Noir custom palette theme missing"
# mlx_max / mlx_revert shell helpers (setup.sh §7)
grep -q "mlx_max" ~/.zshrc 2>/dev/null && ok "mlx_max / mlx_revert helpers in ~/.zshrc" || warn "mlx_max helpers missing from ~/.zshrc"
# ============================================================
# 16. MLX project venv
# ============================================================
section "16. MLX project"
if [[ -d ~/1_PROJECTS/mlx-lab ]]; then
ok "~/1_PROJECTS/mlx-lab exists"
if [[ -f ~/1_PROJECTS/mlx-lab/pyproject.toml ]]; then
ok "uv project initialized"
else
warn "no pyproject.toml — re-run: cd ~/1_PROJECTS/mlx-lab && uv init"
fi
else
miss "~/1_PROJECTS/mlx-lab missing — see setup.sh step 14"
fi
# MLX import test — locked decision: MLX lives in the per-project venv (~/1_PROJECTS/mlx-lab/.venv), NOT global python
MLX_PY="$HOME/1_PROJECTS/mlx-lab/.venv/bin/python"
if [[ -x "$MLX_PY" ]] && "$MLX_PY" -c "import mlx.core as mx; print(mx.metal.is_available())" 2>/dev/null | grep -q True; then
ok "MLX Metal available (mlx-lab venv)"
elif command -v python &>/dev/null && python -c "import mlx.core as mx; print(mx.metal.is_available())" 2>/dev/null | grep -q True; then
ok "MLX Metal available (global python)"
else
warn "MLX not importable from mlx-lab venv or global python"
fi
# ============================================================
# Summary
# ============================================================
echo ""
echo -e "${C_BLUE}═══════════════════════════════════════════════════════${C_RESET}"
echo -e "${C_BLUE} Verification summary${C_RESET}"
echo -e "${C_BLUE}═══════════════════════════════════════════════════════${C_RESET}"
echo -e " ${C_GREEN}✓ PASS: $PASS${C_RESET}"
echo -e " ${C_RED}✗ FAIL: $FAIL${C_RESET}"
echo -e " ${C_YELLOW}⚠ WARN: $WARN${C_RESET}"
if [[ ${#TO_INSTALL[@]} -gt 0 ]]; then
echo ""
echo -e "${C_BLUE}── To install (run setup.sh, or paste below) ──${C_RESET}"
for cmd in "${TO_INSTALL[@]}"; do
echo " $cmd"
done
fi
if [[ ${#TO_REMOVE[@]} -gt 0 ]]; then
echo ""
echo -e "${C_YELLOW}── Dropped items installed — optional cleanup ──${C_RESET}"
for cmd in "${TO_REMOVE[@]}"; do
echo " $cmd"
done
echo ""
echo -ne "${C_YELLOW} Uninstall all dropped items now? (y/N) ${C_RESET}"
read -r answer
if [[ "$answer" =~ ^[Yy]$ ]]; then
for cmd in "${TO_REMOVE[@]}"; do
echo "Running: $cmd"
eval "$cmd"
done
echo -e "${C_GREEN}Cleanup done${C_RESET}"
fi
fi
echo ""
# --status-md flag — regenerate ~/m5-setup/00-STATUS.md so a fresh reader of the gist
# can see WHERE WE ARE at a glance (✅ DONE / 🟡 PARTIAL / ⬜ TODO / ⚠️ DRIFT).
if [[ "$1" == "--status-md" ]] || [[ "$STATUS_MD" == "1" ]]; then
STATUS_OUT="${STATUS_MD_PATH:-$HOME/m5-setup/00-STATUS.md}"
# Detect Prompt run artifacts on disk
_audit=$(ls -t /tmp/m5-audit-*.md 2>/dev/null | head -1)
_install=$(ls -t /tmp/m5-install-fix.log 2>/dev/null | head -1)
_verify=$(ls -t /tmp/m5-verify-*.md 2>/dev/null | head -1)
_debug=$(ls -t /tmp/m5-debug-*.md 2>/dev/null | head -1)
_review=$(ls -t /tmp/m5-review-*.md 2>/dev/null | head -1)
_polish_done=$(grep -q "v2 polish" ~/.config/ghostty/config 2>/dev/null && echo "yes" || echo "no")
_mcps=$(claude mcp list 2>/dev/null | grep -c "^" || echo 0)
_stat() { [ -e "$1" ] && stat -f "%Sm" -t "%Y-%m-%d %H:%M" "$1" 2>/dev/null || echo "—"; }
_row() { [ -n "$1" ] && echo "✅" || echo "⬜"; }
cat > "$STATUS_OUT" <<EOF
# M5 Max — Setup Status
> Auto-generated by \`~/m5-setup/verify.sh --status-md\`
> Last run: $(date '+%Y-%m-%d %H:%M %Z') · PASS=$PASS / FAIL=$FAIL / WARN=$WARN
> Legend: ✅ done · 🟡 partial · ⬜ todo · ⚠️ drift
## Command Center — prompts run
| # | Prompt | Run? | Last run | Artifact |
|---|---|---|---|---|
| 0 | Status | ✅ | $(date '+%Y-%m-%d %H:%M') | \`00-STATUS.md\` (this file) |
| 1 | Audit | $(_row "$_audit") | $(_stat "$_audit") | $_audit |
| 2 | Install | $(_row "$_install") | $(_stat "$_install") | $_install |
| 3 | Ghostty Polish | $([ "$_polish_done" = "yes" ] && echo "✅" || echo "⬜") | $(_stat ~/.config/ghostty/config) | "v2 polish" marker in ghostty config |
| 4 | Claude Code Config | $([ "$_mcps" -ge 4 ] && echo "✅" || echo "⬜") | $(_stat ~/.claude/settings.json) | $_mcps MCPs registered |
| 5 | Verification | $(_row "$_verify") | $(_stat "$_verify") | $_verify |
| 6 | Debugging | $([ -n "$_debug" ] && echo "✅ ran" || echo "n/a") | $(_stat "$_debug") | $_debug |
| 7 | Cleanup + Docs | ⬜ | — | (no on-disk marker — manually tick when done) |
| 8 | Final Review | $(_row "$_review") | $(_stat "$_review") | $_review |
EOF
cat >> "$STATUS_OUT" <<EOF
| Phase | Status | Note |
|---|---|---|
| 1. Foundation (Xcode, brew, arch) | $([ "$PASS" -ge 3 ] && echo ✅ || echo ⬜) | $(uname -m), macOS $(sw_vers -productVersion 2>/dev/null) |
| 2. Brew formulae (44 required) | $([ "$FAIL" -eq 0 ] && echo ✅ || echo 🟡) | run \`brew bundle install --file=~/m5-setup/Brewfile\` if missing |
| 3. Brew casks (32 required) | $([ "$FAIL" -eq 0 ] && echo ✅ || echo 🟡) | font-jetbrains-mono-nerd-font required by Ghostty |
| 4. Dev runtimes (mise: node/python/rust/go) | $(command -v mise >/dev/null && echo ✅ || echo ⬜) | $(mise current 2>/dev/null | head -1 || echo "mise not active") |
| 5. Python tools (slither/halmos/eth-wake via uv) | $(uv tool list 2>/dev/null | grep -q slither && echo ✅ || echo ⬜) | |
| 6. Foundry (forge/cast/anvil) | $(command -v forge >/dev/null && echo ✅ || echo ⬜) | curl -L https://foundry.paradigm.xyz \| bash && foundryup |
| 7. npm globals (claude/vercel/pnpm) | $(command -v claude >/dev/null && echo ✅ || echo ⬜) | $(claude --version 2>/dev/null | head -1) |
| 8. Ollama daemon + models | $(pgrep -x ollama >/dev/null && echo ✅ || echo ⬜) | $(ollama list 2>/dev/null | tail -n +2 | wc -l | tr -d ' ') models installed |
| 9. Configs (.zshrc, ghostty, starship, atuin, mise, claude) | $([ -f ~/.config/ghostty/config ] && echo ✅ || echo 🟡) | |
| 10. Claude Code env in ~/.zshrc | $(grep -q "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC" ~/.zshrc 2>/dev/null && echo ✅ || echo ⬜) | |
| 11. ANTHROPIC_BASE_URL guardrail | $([ -z "$ANTHROPIC_BASE_URL" ] && echo ✅ || echo ⚠️) | $([ -n "$ANTHROPIC_BASE_URL" ] && echo "HIJACKED to $ANTHROPIC_BASE_URL — UNSET" || echo "claude stays on Anthropic cloud") |
| 12. macOS defaults (Finder/KeyRepeat/Dock/dashes) | $([ "$(defaults read -g KeyRepeat 2>/dev/null)" = "2" ] && echo ✅ || echo 🟡) | |
| 13. GPU cap (live + LaunchDaemon) | $([ "$(sysctl -n iogpu.wired_limit_mb 2>/dev/null)" = "122880" ] && echo ✅ || echo ⬜) | $(sysctl -n iogpu.wired_limit_mb 2>/dev/null) MB |
| 14. GPU cap persistence (LaunchDaemon) | $([ -f /Library/LaunchDaemons/com.adam.iogpu-wired-limit.plist ] && echo ✅ || echo ⚠️) | reverts on reboot if missing |
| 15. SSH + GitHub auth | $(ssh -o BatchMode=yes -o ConnectTimeout=3 -T git@github.com 2>&1 | grep -q "successfully authenticated" && echo ✅ || echo ⬜) | gh auth login + gh ssh-key add |
| 16. Git commit signing (SSH via 1Password) | $([ -n "$(git config --global --get user.signingkey)" ] && [ "$(git config --global --get gpg.format)" = "ssh" ] && echo ✅ || echo ⬜) | |
| 17. MLX project (~/1_PROJECTS/mlx-lab) | $([ -d ~/1_PROJECTS/mlx-lab ] && echo ✅ || echo ⬜) | |
| 18. Cleanup (no dropped formulae/casks) | $(brew list --formula 2>/dev/null | grep -qE "^(procs|bottom|httpie)$" && echo ⚠️ || echo ✅) | dropped: procs, bottom, httpie, mstystudio, msty, rectangle, sleeve, wezterm, spotify |
## Raw counts
- ✅ PASS = $PASS
- ⬜ FAIL = $FAIL
- ⚠️ WARN = $WARN
## Next prompts to run
$([ "$FAIL" -gt 5 ] && echo "- **Prompt 2 (Install)** — fill the MISSING list, $FAIL items pending" || echo "- Install phase looks clean")
$(grep -q "v2 polish" ~/.config/ghostty/config 2>/dev/null || echo "- **Prompt 3 (Ghostty Polish)** — apply v2 polish")
$([ -z "$ANTHROPIC_BASE_URL" ] && echo "- (env guardrail green — claude stays on cloud)" || echo "- **CRITICAL: unset ANTHROPIC_BASE_URL immediately**")
$([ "$WARN" -gt 0 ] && echo "- **Prompt 7 (Cleanup + Docs)** — $WARN warnings to triage" || echo "- Cleanup phase looks clean")
---
Refresh this file: \`~/m5-setup/verify.sh --status-md\`
Push to gist: \`gh gist edit fad46568b34a09bf453fd48a57123252 -f 00-STATUS.md ~/m5-setup/00-STATUS.md\`
EOF
echo -e "${C_BLUE} 📋 Status written to $STATUS_OUT${C_RESET}"
fi
if [[ "$FAIL" -eq 0 ]]; then
echo -e "${C_GREEN} ✅ All required items installed${C_RESET}"
exit 0
else
echo -e "${C_RED} ❌ $FAIL items missing${C_RESET}"
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment