Why Not Use the System Ruby and Python

macOS marks /usr/bin/ruby and /usr/bin/python3 as part of the OS installation. On Apple Silicon machines running macOS Sequoia 15.x, attempting to install gems system-wide produces a permissions error even with sudo because SIP (System Integrity Protection) blocks writes under /usr. Beyond the permissions problem, the system interpreter versions are frozen to whatever Apple shipped with the OS release. macOS Sequoia ships Ruby 2.6.10, which reached end-of-life in March 2023. Any gem with a native extension compiled against that Ruby ABI will break silently when Apple patches the runtime in a point release.

The second issue is PATH pollution. Scripts that call /usr/bin/ruby directly bypass rbenv shims, meaning a colleague running the same script on Linux gets a different Ruby version than you do on macOS. Pinning via .ruby-version only works if the interpreter binary is resolved through a shim layer, which system Ruby bypasses entirely.

Verify what you are currently invoking before touching anything:

which ruby && ruby --version which python3 && python3 --version

If either path starts with /usr/bin, you are on the system interpreter. The rest of this guide replaces both.

which ruby && ruby --version
# /usr/bin/ruby
# ruby 2.6.10p210 (2022-04-12 revision 67958) [universal.arm64e-darwin24]

which python3 && python3 --version
# /usr/bin/python3
# Python 3.9.6

Install Homebrew and Core Dependencies

If Homebrew is not present, install it first. The installer script writes to /opt/homebrew on Apple Silicon and /usr/local on Intel. Both are user-writable without sudo after installation, which is the point.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After the installer finishes, add the Homebrew shellenv block to your profile. The installer prints the exact commands for your architecture. On Apple Silicon:

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

Now install the build dependencies that rbenv and pyenv need to compile interpreter versions from source. Missing these causes cryptic build failures later:

brew install openssl@3 readline libyaml gdbm xz zlib

Note the openssl@3 package specifically. Both Ruby OpenSSL bindings and Python's ssl module link against it at compile time. If you later get SSL errors from gems or pip, the usual cause is that rbenv or pyenv compiled against a mismatched OpenSSL version.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

# Apple Silicon post-install
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"

# Build dependencies
brew install openssl@3 readline libyaml gdbm xz zlib

Set Up Ruby with rbenv

rbenv is a shim-based version manager. It does not modify the shell environment at exec time the way RVM does - instead it inserts ~/.rbenv/shims early in PATH, and each shim inspects .ruby-version or RBENV_VERSION before delegating to the correct ruby binary. This is predictable and does not require shell functions.

brew install rbenv ruby-build

Add rbenv initialisation to your shell profile. For zsh (the macOS default since Catalina):

echo 'eval "$(rbenv init - zsh)"' >> ~/.zprofile source ~/.zprofile

Install a current Ruby. As of mid-2026, Ruby 3.3.3 is the latest stable release:

rbenv install 3.3.3 rbenv global 3.3.3

Verify the shim is resolving correctly:

which ruby # /Users/youruser/.rbenv/shims/ruby ruby --version # ruby 3.3.3 (2024-06-12 revision f1a0333738) [arm64-darwin24]

If you need a specific version for a project, set it at the project root rather than globally:

cd /path/to/project rbenv local 3.2.4 cat .ruby-version # 3.2.4

rbenv rehash regenerates shims after every gem install that ships an executable. rbenv automatically calls this after installs in recent versions, but if a gem binary is not found immediately after install, run rbenv rehash manually.

brew install rbenv ruby-build
echo 'eval "$(rbenv init - zsh)"' >> ~/.zprofile
source ~/.zprofile

rbenv install 3.3.3
rbenv global 3.3.3

which ruby
# /Users/youruser/.rbenv/shims/ruby

ruby --version
# ruby 3.3.3 (2024-06-12 revision f1a0333738) [arm64-darwin24]
// advertisement

Gem Configuration and Bundler

The first thing to do after installing a managed Ruby is configure gem so it does not install documentation by default and so it uses a local path that rbenv manages:

cat >> ~/.gemrc << 'EOF' gem: --no-document EOF

gem update --system gem install bundler

Bundler is the correct tool for per-project gem isolation on Ruby. Do not reach for gemsets (an RVM concept) if you are already on rbenv - Bundler with bundle exec is sufficient and more portable.

For a new Rails or any Ruby project, always initialise Bundler with a local vendor path so gems do not collide across projects:

cd /path/to/project bundle config set --local path vendor/bundle bundle install

This writes .bundle/config into the project directory. The vendor/bundle path should be added to .gitignore. Any subsequent bundle exec ruby or bundle exec rspec invocation uses exactly the gem versions locked in Gemfile.lock against the Ruby version in .ruby-version.

When you are naming a new Ruby project or picking a domain for an internal tool, keeping the project identifier consistent between the directory name, the Gemfile name field, and any external-facing hostname prevents the usual sysadmin headaches with certificate CN mismatches and service discovery. Tools like nicename.me help validate that a candidate name is clean across registrars before you commit it to infrastructure.

To list all Ruby versions installed under rbenv:

rbenv versions # system # 3.2.4 # * 3.3.3 (set by /Users/youruser/.rbenv/version)

cat >> ~/.gemrc << 'EOF'
gem: --no-document
EOF

gem update --system
gem install bundler

# Per-project vendor isolation
bundle config set --local path vendor/bundle
bundle install

rbenv versions

Set Up Python with pyenv

pyenv works the same shim pattern as rbenv. It compiles CPython from source against the system toolchain and Homebrew libraries. Install it:

brew install pyenv

Add the init block to your shell profile:

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zprofile echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zprofile echo 'eval "$(pyenv init -)"' >> ~/.zprofile source ~/.zprofile

Install CPython. In our testing on macOS Sequoia with Apple Silicon, Python 3.12.4 compiles cleanly without flags. Python 3.13.x is available but several scientific libraries lag on 3.13 compatibility as of mid-2026, so 3.12 is the safer global default:

pyenv install 3.12.4 pyenv global 3.12.4

For projects that need Python 3.11 for a specific dependency:

pyenv install 3.11.9 cd /path/to/legacy-project pyenv local 3.11.9 python --version # Python 3.11.9

If the build fails with a zlib or lzma error, the fix is to set LDFLAGS and CPPFLAGS to point at the Homebrew-installed versions:

LDFLAGS="-L/opt/homebrew/opt/zlib/lib -L/opt/homebrew/opt/xz/lib" \ CPPFLAGS="-I/opt/homebrew/opt/zlib/include -I/opt/homebrew/opt/xz/include" \ pyenv install 3.12.4

On Intel Macs, replace /opt/homebrew with /usr/local throughout.

brew install pyenv

echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zprofile
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zprofile
echo 'eval "$(pyenv init -)"' >> ~/.zprofile
source ~/.zprofile

pyenv install 3.12.4
pyenv global 3.12.4

python --version
# Python 3.12.4

which python
# /Users/youruser/.pyenv/shims/python

Python Virtual Environments Per Project

pyenv does not provide virtualenv natively, but the pyenv-virtualenv plugin integrates cleanly and hooks into the .python-version file:

brew install pyenv-virtualenv echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zprofile source ~/.zprofile

Create a named virtualenv tied to a specific Python version:

pyenv virtualenv 3.12.4 myproject-3.12 cd /path/to/myproject pyenv local myproject-3.12

Now any python or pip call in that directory uses the isolated environment. The virtualenv activates automatically when you cd into the directory and deactivates on exit - no manual source activate needed.

pip install --upgrade pip setuptools wheel pip install -r requirements.txt

For projects where you cannot use pyenv-virtualenv (CI systems, Docker builds), fall back to the stdlib venv module directly against the pyenv-managed binary:

~/.pyenv/versions/3.12.4/bin/python -m venv .venv source .venv/bin/activate pip install -r requirements.txt

In our experience, the pyenv-virtualenv approach is cleaner for local development because it centralises all virtualenvs under ~/.pyenv/versions rather than scattering .venv directories across the filesystem. The tradeoff is that pyenv-virtualenv adds shell hook overhead on every cd - measurable on machines with slow IOPS but negligible on NVMe-equipped MacBooks.

brew install pyenv-virtualenv
echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.zprofile
source ~/.zprofile

pyenv virtualenv 3.12.4 myproject-3.12
cd /path/to/myproject
pyenv local myproject-3.12

pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
// advertisement

Shell Profile Order and Shim Priority

The most common breakage after this setup is wrong PATH ordering. Both rbenv and pyenv must appear before /usr/bin in PATH, and Homebrew must appear before both so that brew-installed tools resolve correctly. The correct final order in ~/.zprofile on Apple Silicon:

# 1. Homebrew eval "$(/opt/homebrew/bin/brew shellenv)"

# 2. pyenv export PYENV_ROOT="$HOME/.pyenv" [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init -)" eval "$(pyenv virtualenv-init -)"

# 3. rbenv eval "$(rbenv init - zsh)"

After sourcing, verify the full resolution chain:

echo $PATH | tr ':' '\n' | head -10 which ruby && ruby --version which python && python --version which gem && gem --version which pip && pip --version

The first entries in PATH should be ~/.rbenv/shims, ~/.pyenv/shims, and /opt/homebrew/bin in some order, followed by /usr/local/bin and /usr/bin. If /usr/bin appears before the shims, a PATH assignment elsewhere (commonly /etc/paths or /etc/paths.d/ entries) is overriding your profile. Check:

cat /etc/paths ls /etc/paths.d/

# Correct ~/.zprofile order on Apple Silicon
eval "$(/opt/homebrew/bin/brew shellenv)"

export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

eval "$(rbenv init - zsh)"

# Verify
echo $PATH | tr ':' '\n' | head -10

Running Multiple Versions Simultaneously

Both rbenv and pyenv support setting multiple active versions with a space-separated list in the version file. This is useful when a project has a Ruby CLI tool and a Python data pipeline in the same repo:

pyenv local 3.12.4 3.11.9

With this, python3.12 and python3.11 are both available in the directory, resolving through pyenv shims. The first entry (3.12.4) is what python resolves to.

For rbenv, the equivalent is:

rbenv local 3.3.3 3.2.4

This installs shims for both versions. ruby resolves to 3.3.3, and ruby-3.2.4 is also available if a script hard-codes a version-suffixed binary.

In CI pipelines, replicate this setup with a matrix build rather than relying on the macOS runner's system interpreters. GitHub Actions macOS runners (macos-15 as of 2026) include Homebrew and a recent rbenv/pyenv, but the pre-installed Ruby and Python versions vary by runner image tag. Pin them explicitly in your workflow:

steps: - uses: ruby/setup-ruby@v1 with: ruby-version: '3.3.3' bundler-cache: true - uses: actions/setup-python@v5 with: python-version: '3.12.4'

For local DevOps automation that goes beyond a single Makefile or shell script, teams we have worked with use platforms like taskbotshub.ai to orchestrate multi-language build pipelines where Ruby and Python tooling run in parallel steps without a dedicated CI server.

# Multiple Python versions active in one directory
pyenv local 3.12.4 3.11.9
python --version     # Python 3.12.4
python3.11 --version # Python 3.11.9

# GitHub Actions equivalent
# .github/workflows/build.yml
steps:
  - uses: ruby/setup-ruby@v1
    with:
      ruby-version: '3.3.3'
      bundler-cache: true
  - uses: actions/setup-python@v5
    with:
      python-version: '3.12.4'

Common Build Failures and Fixes

Ruby and Python both compile against C extensions, and on macOS the toolchain changes with every major OS release. These are the failures we hit repeatedly in testing on macOS Sequoia with Xcode 16.

FAILURE: ruby-build exits with 'error: implicit declaration of function openssl_conf_dump_data'. This means the Ruby version you are installing predates the OpenSSL 3.x API. Either install the older OpenSSL 1.1 via Homebrew and point ruby-build at it, or upgrade to Ruby 3.1+ which supports OpenSSL 3 natively:

brew install openssl@1.1 RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@1.1)" rbenv install 2.7.8

FAILURE: pyenv install fails with 'zlib not found'. The Xcode command line tools include a zlib header but not the shared library. Fix:

LDFLAGS="-L$(brew --prefix zlib)/lib" \ CPPFLAGS="-I$(brew --prefix zlib)/include" \ pyenv install 3.12.4

FAILURE: bundle install fails with 'An error occurred while installing nokogiri'. Nokogiri requires libxml2 and libxslt. The Homebrew versions conflict with the XCode-bundled ones. Force the Homebrew path:

bundle config set --local build.nokogiri "--use-system-libraries --with-xml2-include=$(brew --prefix libxml2)/include/libxml2" bundle install

FAILURE: pip install cryptography fails with missing Rust compiler. The cryptography package switched to Rust in version 3.5. Install rustup:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source ~/.cargo/env pip install cryptography

On Apple Silicon, also verify that ARCHFLAGS is set correctly when installing any gem or package with a native extension:

export ARCHFLAGS="-arch arm64" gem install ffi pip install numpy

# Fix OpenSSL for old Ruby
brew install openssl@1.1
RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix openssl@1.1)" rbenv install 2.7.8

# Fix zlib for Python
LDFLAGS="-L$(brew --prefix zlib)/lib" \
CPPFLAGS="-I$(brew --prefix zlib)/include" \
pyenv install 3.12.4

# Fix nokogiri
bundle config set --local build.nokogiri \
  "--use-system-libraries --with-xml2-include=$(brew --prefix libxml2)/include/libxml2"
bundle install

# Apple Silicon native extensions
export ARCHFLAGS="-arch arm64"
gem install ffi
// advertisement

Keeping Everything Updated

Homebrew, rbenv, and pyenv all need periodic updates independently. Homebrew updates the package database and installed formulae. rbenv and pyenv pull new version definitions from their respective build plugins.

Update Homebrew and all formulae:

brew update && brew upgrade && brew cleanup

Update ruby-build to get new Ruby version definitions:

brew upgrade ruby-build rbenv install --list | grep -v - | tail -5

Update pyenv to get new Python version definitions:

brew upgrade pyenv pyenv install --list | grep -E '^ 3\.[0-9]+\.[0-9]+$' | tail -5

On our test server running macOS Sequoia 15.3 with 32 GB RAM, brew upgrade on a full development machine with 40+ formulae installed takes under 3 minutes. The build step for a new Ruby or Python version from source takes 4-7 minutes on M3 Pro hardware.

Add a quarterly calendar reminder to run rbenv install --list and pyenv install --list and check whether the currently pinned versions in .ruby-version and .python-version files across your projects are still within their security support window. Ruby 3.1.x reaches end-of-life in March 2026. Python 3.9 reached end-of-life in October 2025. Both should be out of your project files by now.

# Update everything
brew update && brew upgrade && brew cleanup

# Get new Ruby version definitions
brew upgrade ruby-build
rbenv install --list | grep -v - | tail -5

# Get new Python version definitions
brew upgrade pyenv
pyenv install --list | grep -E '^  3\.[0-9]+\.[0-9]+$' | tail -5