macのPATH設定について

About this scraps
macOSの path_helper
について掘り下げて悩んでまとめる

path_helper
とは
はじめにこのコマンドの概要を整理する。

macOSの /usr/libexec/path_helper
にデフォルトで存在するコマンド。man曰く、
helper for constructing
PATH
environment variableThe path_helper utility reads the contents of the files in the directories
/etc/paths.d
and/etc/manpaths.d
and appends their contents to thePATH
andMANPATH
environment variables respectively.
つまり特定のディレクトリ以下にあるファイル群からPATH
を構成・出力するもの。


シェル起動時に -l
を与えるとログインシェルとして起動する。
現在のシェルプロセスがログインシェルかどうかは以下のスクリプトで確認できる[1]:
if [[ -o login ]]; then echo "login"; else echo "not login"; fi # 'login' or 'not login'
ターミナル.appでは、「デフォルトのログインシェル」が選択されている場合は毎回 -il
が渡される = ログインシェルかつインタラクティブシェルとして起動する
% ps aux | grep /bin/zsh
user 26312 0.0 0.0 410808784 4560 s008 Ss+ 3:38PM 0:00.34 /bin/zsh -il
VSCodeはデフォルトで -l
が渡される (terminal.integrated.profiles.<platform>
で設定できる[2])

明示的にオプションが渡されなければログインシェルにはならない。
- パスを指定して実行した場合 (
./script.sh
,/path/to/script.sh
)、スクリプトが実行されるのは非ログインシェルとなる。shebangで-l
を設定すると、再度ログイン処理(?)が走る。 - built-in command[1] の
.
で実行した場合(. ./script.sh
)は、現在の環境を引き継ぐため、スクリプトが実行されるのはログインシェルとなる。 -
zsh
に直接渡した場合は (当然ながら) コマンドラインオプションに依存する。

path_helper
問題とは
ここでは 2025年1月時点での path_helper
が引き起こしている問題の概要を整理する。

要約すると シェル起動時にPATH
の順序が変わってしまう 問題。
python
や ruby
, nano
など、macOSにデフォルトで入っているプログラムを自分でインストールした場合、macOSのデフォルトが優先されてしまうことがある。
影響を受けるのがPATH
というuser-dependentな値であること、また歴史的経緯が非常に複雑なことから、2025年現在でも完全無欠の解決策は示されていない(調査した限り)。


なお最近[1]のmacOSでは PATH
の早い段階に /usr/local/bin
がくるようになったため、この問題が発生しない環境も存在する。具体的には
- Yosemite以降のmacOSを搭載したIntel mac
- Homebrewが
/usr/local/bin
にインストールされており、バイナリのインストール先が/usr/local/Cellar
で、シンボリックリンクが/usr/local/bin
に張られている -
~/.zshenv
,~/.zprofile
.~/.zshrc
にPATH
を操作するコードがない - ターミナル.appまたはVSCode Integrated terminalを使っているか、シェルの起動引数に
-l
が渡されている - インタラクティブシェルで直接コマンドを実行する
以上の条件を満たす場合、Homebrewでインストールしたパッケージについて path_helper
で苦労することはほぼない。
例外としては、pyenvやrbenvなど macOSビルトインと同名のコマンドを /usr/local/bin
以外に配置して使う ことを想定しているソフトウェアが挙げられる。
-
OS X Yosemite以降, 後述 ↩︎

歴史
OS X Leopard で追加されて以来、 path_helper
はさまざまな問題を引き起こしてきた。
ここではその歴史を振り返る。


2009/08/28: OS X 10.6 Snow Leopard リリース
path_helper
の実装がシェルスクリプトからバイナリに変更された。また、当時は opensource.apple.com にてソースコードが公開されていた(2021年に削除, 現在でもアーカイブ[1]は閲覧可能)。

path_helper
の呼出し箇所に関する議論
2013/01/27: zshのフレームワークpreztoのリポジトリにて「path_helper
を無効化してはいけない」という旨のissueが起票された[1]。ここでは様々な論点・立場から激しい議論が繰り広げられた。
- インストーラ等により使われる可能性があるから、
path_helper
を無効化してはいけない -
path_helper
は/etc/zshenv
ではなく/etc/zprofile
で呼ばれるべきだ -
path_helper
はPATH
を並べ変える "壊れた" コマンドであるから、無効化すべきだ - そもそもzshが
/etc/zshenv
以外のグローバルrcファイルを読まないようにすべきだ - ユーザが
/etc/paths
を直接書き換えるべきだ
いずれも「解決できること」と「解決できないこと」または「懸念として残ること」があった。
解決策は見つからぬまま、このissueは2日後にリポジトリのオーナーによってクローズされた。
冷静に考えると、 /etc/zshenv
で path_helper
が実行されているなら、 ~/.zshenv
で設定した PATH
は正しく設定されるような気もする(~/.zshenv
の方が後に読み込まれるため)。
しかし path_helper
はそれまでの PATH
の値を引き継がないので(Leopardでのバグ修正によるもの)、非インタラクティブまたは非ログインシェルでは ~/.zshrc
や ~/.zprofile
で設定した PATH
が反映されないという懸念がある。

2014/10/16: OS X 10.10 Yosemite リリース
このタイミングで /etc/paths
の内容が変更され、 /usr/local/bin
が先頭にくるようになった[1][2][3]。この影響かHomebrewの動作が不安定になり、issueが起票された[4]。
これがどのような意図で行われたのかは不明で、2025年現在も /etc/paths
のエントリ順は変わっていない。
この変更により、多くの環境で path_helper
は問題視されなくなった。理由は以下のとおり。
- Macを使う開発者の多くはHomebrewを使用している
- Homebrewは
/usr/local/Cellar
にコマンドの実体、/usr/local/bin
にそのシンボリックリンクを張る -
/etc/paths
という最上位のファイルで/usr/local/bin
が優先されるようになったため、相当変なことをしない限りHomebrew版のコマンドを問題なく呼び出せる
-
bash - OS X 10.10.4 Yosemite, has the default PATH changed? - Stack Overflow ↩︎
-
macos - What is the Default content in /private/etc/paths? - Ask Different ↩︎
-
Mac OS X YosemiteにPHPの開発環境を構築してみる(1) Xcode、Homebrew編 - ykiraの開発ブログ ↩︎
-
(10.10) not recognizing /usr/local/bin before /usr/bin · Issue #29843 · Homebrew/legacy-homebrew ↩︎

2015/09/30: OS X El Capitan リリース
このバージョンから、path_helper
の呼び出しコードが /etc/zprofile
に移動した[1][2][3]。
これは前述のissueで議論が紛糾したため……もあるかもしれないが、 他シェルとの挙動を揃える意図 があったのではないだろうか?
例えばbashでは path_helper
の呼び出しコードは /etc/profile
に記述されているため、ログインシェルとして起動した場合にのみ実行される。これと同様の動作をzshで実現するために、 /etc/zprofile
へ呼び出しコードが移動された……とも推測できる。
なお、この変更により path_helper
が ~/.zshenv
より後で実行されるようになったため、我々は ログインシェルにおいて ~/.zshenv
で素直に PATH
を設定する手段を失ってしまったことになる。
やるとすれば別の変数で受けて ~/.zshrc
で反映させるか、非ログインシェルとして起動する (ターミナルの起動引数に -l
を渡さない) しかない。

--disable-etcdir
の削除
2018/11/24: zshのインストールオプション Homebrewの方針変更に伴い、zshのインストールオプション --disable-etcdir
が削除された[1]。
このオプションは、zshが /etc/zshenv
以外の /etc/z*
を読まないようにするためのものである。これを用いた解決策は2012年ごろからインターネット上で共有されていたが、この変更に伴い使用できなくなった。
なお2025年1月16日現在、brewのzshには --enable-etcdir=/etc
が渡されており[2]、/etc/z*
を読むものとしてビルドされている。

2020/11/17: Apple Siliconを搭載したMacの発売開始
いわゆる M1 Mac の最初のモデルが発売された。HomebrewはIntel-Macでは /usr/local/bin
、そうでなければ /opt/homebrew/bin
にインストールされるようになった。
これに伴い後者にはデフォルトでPATH
が通らなくなったため、brew導入時にこのようなメッセージが出力されるようになった:
==> Next steps:
- Run these two commands in your terminal to add Homebrew to your PATH:
(echo; echo 'eval "$(/opt/homebrew/bin/brew shellenv)"') >> /Users/username/.zprofile
eval "$(/opt/homebrew/bin/brew shellenv)"
曰く brew shellenv
の出力を ~/.zprofile
にペーストせよとのこと。brew shellenv
とは:
Print export statements. When run in a shell, this installation of Homebrew will be added to your PATH, MANPATH, and INFOPATH.
export 文を出力します。シェルで実行した場合、Homebrewのインストールが PATH、MANPATH、および INFOPATH に追加されます。
つまりbrewのために PATH
をいい感じに設定するスクリプトを生成・出力してくれるコマンドである。

解決策とされる手法の考察
上述のとおり、path_helper
はその実装以来多くの問題を引き起こしてきた。課題として浮上するたびに様々な解決策(とされる手法)が提起・検討されてきた。
これまで提案されてきたそれら手法について、個人的見解を主にまとめる。

/etc/zshenv
を /etc/zprofile
に mv
する
解決策1. 要約: OS X El Capitan 以降では効果がない。
El Capitan以前のmacOSでは /etc/zshenv
が存在し、そこで path_helper
が呼ばれていた。そのためサブシェルを立ち上げるたびに PATH
がリセットされてしまい、問題となった。
現在では(クリーンインストールであれば) /etc/zshenv
は存在せず、path_helper
の呼び出しコードは /etc/zprofile
に移動した ため、効果はない。

path_helper
をなんらかの方法で無効化あるいはバイパスする
解決策2. 要約: 予期せぬ不具合を生じ得る。
chmod
で実行権限を剥奪したり、 no_global_rcs
を有効化したり、 --disable-etcdir
をconfigure
に入れてビルドしたりして path_helper
を無効化することができる。
しかし一部のソフトウェア(特にインストーラで入れるタイプのもの)は path_helper
の動作を前提とするものがある ため、健康的な解決策とは言い難い。
具体的には /etc/paths.d/
にエントリを追加し、自身へ PATH
を通す処理を path_helper
に任せてしまうというもの。2025年現在でも.NET Framework for macやGolangなどがこの方式を採用している。
PATH
くらい自分で通すからいいぜ!という主張もあるかもしれないが、path_helper
に悩むすべてのユーザにそれを強いるのは、果たして美しい解決策だろうか?

/etc/paths
あるいは /etc/paths.d
を書き換える
解決策3. 要約: 予期せぬ不具合を生じ得る。
~
より上のファイルを単一ユーザのために書き換える行為はマルチユーザ環境で予期せぬ不具合を生じる可能性が高いため、すべてのユーザに推奨できる方法とは言い難い。
まさか /etc/paths
に /Users/username/bin
などと書くわけにはいかない。

path_helper
の代替実装をインストールする
解決策4. 要約: 外部プログラムに根幹を任せてよいのか?
GitHub等で検索すると、path_helper
の代替を謳うリポジトリがいくつか見つかる。これらの一部には今でも正しく動作するものもあるかもしれないが……
代替実装のソースを全て読み、動作とその安全性を確認し、運用する…… 果たして、PATH
のためにそこまでする必要はあるだろうか?

PATH
を ~/.zshrc
で設定する
解決策5. 要約: スタートアップファイルの目的にそぐわないのでは?
.zshrc
はzshがインタラクティブシェルとして起動した際 path_helper
より後に読み込まれるため、ほとんどの場合はこれで解決する。
しかしzshのintroductionによれば .zshenv (中略) should contain commands to set the command search path, plus other important environment variables. とあることから[1]、本来 PATH
の設定コードは ~/.zshenv
におくのが望ましい といえる。
また、非インタラクティブシェルでは読み込まれないこと、対話型シェルをサブシェルとして起動するたびに再設定されることなどを踏まえると、.zshrcに置くのはその目的からやや外れているといえる。

結論
調査結果をもとに2025年1月現在最適な構成を考える。
- OS: macOS Sonoma あるいは Sequoia
- シェル: zsh
-
目的:
-
path_helper
問題の解決 - OS間ポータビリティを維持したスタートアップファイルの構成
-

(改めて) 問題整理
path_helper
はmacOSにのみ存在するコマンドである。/etc/zprofile
で呼び出され、~/.zshenv
で設定した PATH
の順番を書き換えてしまう。そのため、 python
, ruby
, sed
などmacOSビルトインのものと同名のコマンドを自らインストールした際、パスによってはシステムデフォルトのものが優先されてしまう場合がある。
path_helper
は無効化すべきでないため、我々はどうにか /etc/zprofile
より後に PATH
を編集 しなければならない。ここで "編集" とは、ログインシェルあるいは派生した環境[1] でのみ 、システムデフォルトより自分でインストールしたコマンドが優先されるようにPATHの順序を入れ替える操作を指す。

簡単な解決策
最も容易な解決策は ~/.zprofile
~ ~/.zlogin
までのいずれかのファイルで PATH
を設定する 方法である。一般的なシチュエーションではこれで解決する。
なぜ解決するのか
先述したファイル群はそれぞれ読み込まれる条件が異なるのに、なぜそんなテキトーな方法で解決するのか?
~/.zprofile |
~/.zshrc |
~/.zlogin |
|
---|---|---|---|
ログインシェル | ◯ | ◯ | |
インタラクティブシェル | ◯ |
macOSの一般的なターミナルエミュレータ (ターミナル.appやVSCode統合ターミナルなど) では、起動時の引数に -l
が渡される。つまり毎回ログインシェルとして起動するため、何も考えずにGUIからターミナルを開くぶんには、これらファイルはどこかのタイミングで全て読み込まれる。
「おい、シェルスクリプトから実行するときに困るじゃないか」?
困らない。なぜなら シェルスクリプトを叩くときは基本的になんらかのログインあるいはインタラクティブシェルを経由するはず だからだ。
zshでは PATH
にはエクスポート属性が設定されている。したがって、シェルスクリプトの実行環境を継承した親環境のうちのどこかで PATH
が設定されていれば、問題なく動作する。
ゆえにこのラフな手法でなんとかなる……というより、なんとかなってしまう。

落とし穴
さて、このラフな手法には 意外な落とし穴 がある。
ssh
経由で直接コマンドを実行
1. macOSでは、「システム環境設定」>「一般」>「共有」>「リモートログイン」を有効化することでsshサーバが起動する。
では、 ssh
に直接コマンドを渡してみよう。
% ssh 127.0.0.1 printenv
(省略)
(user@127.0.0.1) Password:
LANG=ja_JP.UTF-8
(省略)
PATH=/usr/bin:/bin:/usr/sbin:/sbin
(省略)
path_helper
も動作しないし、 ~/.zshrc
で設定したパスも設定されない。なぜならこのシェルは非ログインかつ非インタラクティブなシェルだからだ。この場合、zshが読み込むのは /etc/zshenv
と ~/.zshenv
のみとなる。
2. ショートカット.app から実行
ショートカット.app には シェルスクリプトを実行 というブロックがある。
当然ながら、このシェルは非ログインかつ非インタラクティブなシェルである:
なら printenv
も同じ値になるのか……と思いきやそんなことはなく、なんと path_helper
準拠のパスが設定される。なのに ~/.zprofile
は呼ばれない 。また ~/.zshenv
は呼ばれるが、 path_helper
が出力するパスはそれより後に来る。
……ユーザが望んでいたのは、この環境だったんじゃないか……?
脱線したが、まとめると
-
~/.zprofile
以降のファイルでPATH
を再設定するのは、簡単な解決策- 現在のmacOSにおけるほとんどのユースケースではこれで解決してしまう
- いくつか落とし穴がある
- ssh経由でインタラクティブシェルを介さずコマンドを実行した場合
- ショートカット.app から実行した場合

解決案
健康的に回避・解決する手段はいくつか考えられる。
~/.zshenv
と ~/.zprofile
で変数を受け渡す
1. ~/.zshenv
で PATH
の設定を完了し、「Darwinかつログインシェルなら、環境変数 PATH_BACKUP
に現時点の PATH
を保存する」処理を入れる。~/.zprofile
で PATH_BACKUP
を受け取り、PATH
に反映する。
これで、path_helper
の出力を尊重しつつ PATH
をいい感じにすることができる。
- メリット: 環境の変更が少ない。
- デメリット: 煩雑。
~/.zshenv
で no_global_rcs
してから path_helper
を呼ぶ
2. under-controlでない部分をスキップしつつ、意図的に path_helper
を呼び出し、その後でゆっくりPATHを編集するというもの。GitHubではKOBA789氏のdotfilesに採用されている[1]が、初出は不明。もしかすると氏が初出かもしれない。
- メリット: 構造が単純。
-
デメリット:
no_global_rcs
しているので、~/.zshrc
で/etc/zshrc
をsource
する必要がある。
~/.zpath
的なファイルを作り source
する
3. PATH
の設定を別ファイルに退避させ、 ~/.zshenv
や ~/.zprofile
で読み込むというもの。
-
メリット:
PATH
を外部に置けるので、必要箇所で読み込めばなんとかなる。 - デメリット: 非標準のファイルが増える。