How to Diagnose a Slow Mac with Gemini CLI (and Why I Switched to Zed)

Posted by Michael S. on March 13, 2026

I recently found myself struggling with a Mac that would randomly run hot and loud, slowing down right when I needed it most. My setup was bloated. I had VS Code and Cursor eating up memory, Firefox tabs with memory leaks (a single Google Play Console tab once consumed 5 gigabytes of RAM), and OneDrive's fileproviderd running for hours even after quitting the app.

To top it off, I realized that OBS—which I use for recording my screen—was throttling the machine so heavily that I couldn't properly record the very things I was trying to capture. (It reminds me of the observer effect in physics, where the very act of observing a system changes it.) I also didn't know just how much swap space I'm constantly using as my RAM stays near full at all times.

I needed a quick way to diagnose what was causing my Mac to slow down in the moment.


The diagnose-mac Script

Google offers several limits for Gemini API users: the web consumer interface, the Antigravity limit (which I usually max out), and the Gemini CLI. The problem with Google's CLI tool is that it takes forever. It literally takes about 5 seconds to "load cached credentials," as they call it, and then another 5–10 seconds before it outputs the first word from gemini-3.1-pro-preview. They turned what should be a simple terminal utility into desktop-level bloat.

Despite the excruciating startup time, I created a bash alias that pipes critical Mac diagnostics straight into Gemini CLI for deep analysis. I've had it in my .zshrc for less than a week, and it has already revolutionized my workflow.

Here is the script:

~/.zshrc
# 2026-03-09 (updated 2026-03-23)
diagnose-mac() {
  local copy_to_clipboard=false

  # Check for flags
  case "$1" in
    -c|--copy-data)
      copy_to_clipboard=true
      ;;
  esac

  # Validate sudo upfront so the user knows their password was accepted
  sudo -v
  echo "Password accepted. Gathering data for Gemini... this may take a moment."

  # Capture the diagnostics into a variable
  local diagnostics
  diagnostics=$(
    echo -e "Please figure out what's wrong with my computer. Why is it running hot and/or slow today, and why are the fans acting up? Are there any CPU or RAM hogs?\n\n"
    echo -e "<output cmd=\"top_cpu\">\n$(top -l 2 -o cpu -n 15)\n</output>\n"
    echo -e "<output cmd=\"top_mem\">\n$(top -l 1 -o mem -n 15)\n</output>\n"
    echo -e "<output cmd=\"pmset\">\n$(pmset -g therm)\n</output>\n"
    echo -e "<output cmd=\"powermetrics\">\n$(sudo powermetrics -n 1)\n</output>"
  )

  # Handle clipboard logic
  if [ "$copy_to_clipboard" = true ]; then
    echo "$diagnostics" | pbcopy
    echo "šŸ“‹ Data saved to clipboard."
  fi

  # Pipe to Gemini for analysis
  cd /tmp && echo "$diagnostics" | gemini -m gemini-3.1-pro-preview -p "Analyze these diagnostics for performance issues using your deep reasoning/thinking capabilities. Identify the specific PID or process causing issues."

  echo -e "\nāœ… Analysis complete."
}

When my Mac acts up, I just run diagnose-mac and let the AI pinpoint exactly which process is the culprit. It's incredibly straightforward and gets the job done. I can't tell you how much this session would cost if I ran it and was paying for it, but it's definitely worth every penny.

Update (March 23): I added sudo -v at the top of the function. This validates your password upfront (without running any command) and then prints a confirmation message before the slow data-gathering step begins. Previously, you'd enter your sudo password and get nothing back. The whole reason I'm running this script is because my computer is slow, so I'm already impatient. After a few seconds of silence I'd figure the password went through since it didn't ask me to retype it, and I'd move on to something else. But I was never sure. Now it tells you right away that the password was accepted and it's gathering data.


Moving to Zed

The bloat in VS Code, Cursor, and the Gemini CLI desktop ecosystem finally pushed me over the edge. I installed Zed (via Homebrew: brew install zed), the Rust-based code editor.

My first impression? Wow, it's fast.

The Vim integration in Zed is an absolute killer feature. It's my new favorite editor, and incidentally, this very blog post is the first file I've written entirely in Zed. The speed difference compared to Electron-based editors is night and day.

One detail that really streamlines the workflow is how you navigate between your code and the AI agent. Zed uses a toggle system for the agent panel (where Gemini lives): you can jump back and forth by pressing Cmd + Shift + / (which is Cmd + ?). If you are in the editor, this shortcut opens and focuses the agent panel. If you are already in the agent panel, it toggles focus back to the editor. Alternatively, you can always switch focus straight back to the first editor pane by hitting Cmd + 1. I also frequently use Cmd + J to quickly toggle the terminal, which is where I keep another Gemini instance running for when I need terminal-specific commands.

Action Zed Cursor Antigravity
Open Agent Cmd + Shift + / Cmd + L Cmd + L
Toggle Mode (YOLO) Cmd + I Cmd + . / Cmd + Shift + .
Change Model Cmd + Option + .* Cmd + . Cmd + .

*In Zed, you currently use the Command Palette (Cmd + Shift + P) and search for "model," or Tab to the model selector in the assistant panel. It replaces the Cmd + . shortcut I'm used to in Cursor and Antigravity.

In Cursor, Cmd + . or Cmd + Shift + . opens the mode switcher for the agent — Agent, Plan, Debug, and Ask modes in @Cursor. Unlike Zed, Cursor and Antigravity don't deeply integrate their agent chat with the terminal sessions themselves (Cursor has a dedicated Claude Code tab instead), so I still keep separate terminals for long-running commands.

It's worth noting that Zed appears to phone home: even after I disabled all the telemetry options I could find in the settings, I saw it hitting external IPs. For instance, I caught it trying to reach Cloudflare IPs (like 104.18.6.151). I checked my firewall logs with LuLu just to be sure:

$ log show --predicate 'subsystem == "com.objective-see.lulu" and eventMessage contains "Zed"' --info --last 2h
Timestamp App Action Destination
02:01:49 /Applications/Zed.app blocked 104.18.6.151:443
02:33:01 /Applications/Zed.app blocked 104.21.28.29:443

Update (March 15, 12:34 a.m.): Quick note: I did have auto-update on at the time.

Mea culpa. I've since checked Zed's repository on GitHub (https://github.com/zed-industries/zed) and confirmed that it wasn't phoning home. It was just checking for updates. The editor does that over the network (and it uses rsync, which is a built in command and that's pretty cool). So those blocked connections were update checks, not telemetry. Here's what actually runs:

  • cloud.zed.dev / api.zed.dev: Primary API for the latest version, release manifests, and language server/extension update checks. Example: https://cloud.zed.dev/releases/stable/latest/asset?asset=zed&os=macos&arch=aarch64. These domains are fronted by Cloudflare, so you'll see standard Cloudflare edge IPs (e.g. 104.26.11.80, 172.67.73.169, 104.26.10.80); the exact IPs depend on your location.
  • github.com / api.github.com: The actual binaries (e.g. the .dmg or .tar.gz) are apparently served from GitHub Releases. Those resolve to Fastly/GitHub infrastructure.
  • zed-extensions.nyc3.digitaloceanspaces.com: While checking for editor updates, Zed also checks for extension and Language Server (LSP) updates. Some of those payloads are hosted on DigitalOcean Spaces.

(A quick note: Zed Pro is free for students, but be careful. Once you exceed your $5/month API limit, you actually pay 10% more for API calls than you would if you didn't pay for Pro! I haven't signed up for Pro myself yet, since I haven't seen the benefits. However, if you already have Gemini CLI, Claude Max, or OpenAI Codex, it might actually make sense to get it soon, since you don't have to pay for additional api calls. For comparison, the only reason I keep a student Cursor subscription is for the built-in usage quota to write blog posts and do some other tasks. I'm sure Zed will ship something eventually that makes it undeniably worth it. Until then, because it's open-source, you can theoretically get most of the benefits for free just by pulling the repository and compiling it from source with rustup if you really need to.)

On a broader note, my workflow is quickly becoming adopted by everyone. I used to rely heavily on my own scheduler, and now OpenAI Symphony has an Elixir that mostly matches it. I've also been using Antigravity, and it's getting pretty good too. So my Claude team might soon have competitors—or more teammates, if I use them right!


Bonus: Old AI Scripts from 2024

While looking through my dotfiles for the diagnose-mac command, I stumbled upon a couple of aliases I wrote back in September 2024, when LLMs were just starting to get really good.

One is a quick Python script to generate git commit messages based on staged changes, and the other automates the process of publishing to PyPI (which tells me I must have created a PyPI package that same day, probably because someone asked for something on Hacker News and I built it for them).

Here's the git-generate alias and the Python code it calls:

~/.zsh_aliases
# 2024-09-26
alias git-generate='python ~/OneDrive/Documents/2024/projects/generate-git-commit-message-from-staged-changes.py'
generate-git-commit-message-from-staged-changes.py
import sys
import urllib.request
import json
import subprocess
import os
import pyperclip

# Check if we are in a git directory
try:
    subprocess.check_output(["git", "rev-parse", "--is-inside-work-tree"])
except subprocess.CalledProcessError:
    exit(128)

# Get the OpenAI API key from the plist file
api_key = subprocess.check_output([
    "defaults", "read",
    os.path.expanduser("~/Library/Preferences/org.smolkin.plist"),
    "openaiApiKey"
]).strip().decode("utf-8")

if len(sys.argv) > 1:
    concise_or_verbose = sys.argv[1]
else:
    concise_or_verbose = "concise"

# Get the git diff output and escape special characters
git_diff_output = subprocess.check_output(["git", "diff", "--cached"]).decode("utf-8")
escaped_git_diff_output = git_diff_output.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace('"', "&quot;").replace("'", "&#39;").replace("/", "&#47;").replace("\\", "\\\\")

data = {
    "model": "gpt-4.1",
    "messages": [
        {
            "role": "system",
            "content": f"You are a helpful assistant that reads git-diff output and generates {concise_or_verbose} git commit messages. You may create the commit message in the form of a single line or multi-line. You may use the following format: <type>(<scope>): <Subject>"
        },
        {
            "role": "user",
            "content": f"Generate a {concise_or_verbose} git commit message based on the following changes (if no changes are provided, return the following message: 'No staged changes. Stage your changes and try again.'):"
        },
        {
            "role": "user",
            "content": escaped_git_diff_output
        }
    ],
    "temperature": 0,
    "max_tokens": 300
}

json_data = json.dumps(data).encode("utf-8")
url = "https://api.openai.com/v1/chat/completions"
headers = {
    "Content-Type": "application/json",
    "Authorization": f"Bearer {api_key}"
}
req = urllib.request.Request(url, data=json_data, headers=headers, method="POST")

with urllib.request.urlopen(req) as response:
    response_data = response.read().decode("utf-8")
    response_json = json.loads(response_data)
    commit_message = response_json['choices'][0]['message']['content']
    print(commit_message)
    pyperclip.copy(commit_message)

And the corresponding PyPI publisher alias:

~/.zsh_aliases
# 2024-09-30
alias pip-publish='
    git-generate
    read -p "Did you remember to update the version in setup.py and main.py? Press enter to continue uploading..."
    read -p "Did you remember to stage your changes? Do you want to git commit? (y/N): " commit_choice
    if [[ "$commit_choice" == "y" || "$commit_choice" == "Y" ]]; then
        git commit
    fi
    git push
    pip install --upgrade setuptools wheel twine && rm -rf dist && python setup.py sdist bdist_wheel && twine upload dist/*
'

Enjoyed this post?

Get notified when I publish something new. No spam, unsubscribe anytime.