手で叩けば動くのに、launchdに任せた途端「そんなファイルはありません」で止まる。うちでも自動化を作り始めた頃に一番混乱したのがこれでした。
結論から書きます。launchdは、スクリプトが置いてあるフォルダからは起動してくれません。だから ./設定.json や 記事一覧.tsv のような「フォルダ名を省いた書き方」(相対パス)は、手で動かすときだけ通って、launchdでは通りません。直し方は3つあり、うちでは使い分けています。この記事はその3つと、いま自分が踏んでいるかを30秒で確かめる方法です。
なぜ手で動かすと通ってしまうのか
ターミナルでスクリプトを動かすとき、たいてい人はそのフォルダに移動してから動かします。
cd ~/scripts/media
./ネタ補充.sh
このとき、スクリプトから見た「いまいる場所」は ~/scripts/media です。だから中で 記事一覧.tsv とだけ書いてあっても、ちゃんと ~/scripts/media/記事一覧.tsv が読めます。
launchdには、この「いまいる場所」がありません。人が cd してから呼ぶわけではないからです。man launchd.plist にはこう書かれています。
WorkingDirectory — ジョブを走らせる前に
chdirする先を指定する、任意のキー。
「任意の」というのが要点です。書かなければ、どこから起動されるかはこちらの都合とは無関係に決まります。少なくとも「スクリプトが置いてあるフォルダ」にはなりません。だから相対パスが総崩れになります。
しかも厄介なことに、この失敗はふだんの動作確認をすり抜けます。人がテストするときは必ず cd してから叩くので、テストでは100%通ってしまう。launchdに登録した翌朝、初めて壊れます。
いま自分が踏んでいるか、30秒で確かめる
推測しないで、実際にどこから起動されているかを1回だけ吐かせるのが確実です。次の3行だけのスクリプトを作ります。
cat > ~/scripts/場所しらべ.sh <<'EOF'
#!/bin/zsh
echo "[$(date '+%Y-%m-%d %H:%M')] いまいる場所: $(pwd)" >> "$HOME/場所しらべ.txt"
echo "[$(date '+%Y-%m-%d %H:%M')] 自分の置き場: ${0:A:h}" >> "$HOME/場所しらべ.txt"
EOF
chmod +x ~/scripts/場所しらべ.sh
これを、いま疑っているplistの ProgramArguments に一時的に差し替えて1回動かすか、いちばん手軽なのは、疑っているスクリプトの先頭にこの2行をそのまま貼ることです。翌朝 ~/場所しらべ.txt を開けば、launchdが実際にどこから呼んだかが1行で分かります。
cat ~/場所しらべ.txt
「いまいる場所」が自分の想定と違っていれば、それが原因です。確かめ終わったら2行を消してください。
直し方は3つ。うちの使い分け
① いちばん堅い:$HOME 起点の絶対パスで書く
うちのメディア自動化は、設定を1か所にまとめて、そこで全部フォルダ名から書いています。~/scripts/media/config.sh の中身がこれです。
# 記事HTMLが置いてあるフォルダ
MEDIA_KIJI_DIR="${MEDIA_KIJI_DIR:-$HOME/scripts/media/記事}"
# どのファイルをどのタイトル・スラッグで出すかの対応表
MEDIA_LIST="${MEDIA_LIST:-$HOME/scripts/media/記事一覧.tsv}"
# Python(このMacは /usr/bin/python3 が動かないので venv のものを使う)
MEDIA_PY="${MEDIA_PY:-$HOME/scripts/keiri/.venv/bin/python}"
MEDIA_HOME="$HOME/scripts/media"
そして読み込む側も、読み込む相手の場所まで絶対パスで書きます。
source "$HOME/scripts/media/config.sh"
ここを source ./config.sh と書きたくなりますが、それをやると設定ファイルそのものが読めずに落ちます。いちばん最初の1行が、いちばん相対パスに書き換えたくなる場所です。ここだけは絶対パスにしてください。
② 引っ越しても壊れない:スクリプト自身の場所から測る
フォルダごと移動するかもしれないものは、自分の置き場を自分で調べさせます。zshならこの1行です。
BASE="${0:A:h}"
$0 が「自分自身」、:A が「省略なしのフルパスに直す」、:h が「ファイル名を落としてフォルダだけにする」。つまり「このスクリプトが置いてあるフォルダ」が入ります。1つ上の階層が欲しければ :h をもう1回足して ${0:A:h:h} です。
うちの旅行メディア側のスクリプトはこの書き方で、scripts/ の中から1つ上を基準にしています。
BASE="${0:A:h:h}"
"$PY" "$BASE/scripts/週2回公開.py" "$@"
これなら、フォルダごと別の場所に移してもそのまま動きます。$HOME 起点の絶対パスだと、移した瞬間に全部書き換えになります。「置き場所が変わりうるか」で①と②を選んでください。
③ どうしても相対パスが要るとき:先頭で自分から cd する
中で呼ぶ他のコマンドがカレントフォルダを見る作りだったり、相対パス前提の道具に渡したりするときは、スクリプトのいちばん上で自分から移動してしまいます。うちの自動公開はこれです。
cd "$HOME/scripts/media" || exit 1
source "$HOME/scripts/media/config.sh"
|| exit 1 を必ず付けてください。移動に失敗したまま先に進むと、まったく違うフォルダで処理を続けてしまいます。これは「見つからない」で止まるより、はるかにたちが悪い。
plistの側に WorkingDirectory を書いても同じことができます。
<key>WorkingDirectory</key>
<string>/Users/あなたのユーザー名/scripts/media</string>
うちはこちらを使っていません。理由は1つで、plistに書くと「手で動かしたとき」には効かないからです。手動とlaunchdで前提が変わる作りは、また同じ罠を踏みます。スクリプトの先頭で cd しておけば、どちらから呼んでも同じ場所から始まります。設定は、動かし方が変わっても効くほうに置く。これが3つのうちで一番大事な判断です。
plistの中では ~ が使えない
相対パスと並んでよくやるのがこれです。plistの中の ~/scripts/media/1本作る.sh は展開されません。~ を「ホームフォルダ」に読み替えるのはシェルの仕事で、plistを読むlaunchdはそれをやらないからです。~ という名前のフォルダを探しに行って、無いので落ちます。
うちのplistは、実行するスクリプトもログの出し先も、全部 /Users/ から書いています。
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/あなたのユーザー名/scripts/media/1本作る.sh</string>
</array>
<key>StandardOutPath</key>
<string>/Users/あなたのユーザー名/scripts/media/logs/launchd_generate_out.log</string>
ログの出し先を ~ で書いてしまうと、失敗しても、失敗の理由を書いたログ自体がどこにも出ません。原因調査の入口を自分で塞ぐことになるので、ここは特に気をつけてください。
Pythonに渡すときも、場所を持たせて渡す
シェルからPythonを呼ぶ構成だと、Python側でまた同じ問題が起きます。うちはPythonに場所を推測させず、シェルが決めた絶対パスを環境変数で手渡ししています。
# シェル側(toukou.sh)
source "$HOME/scripts/media/config.sh"
export MEDIA_KIJI_DIR MEDIA_LIST MEDIA_AUTH_FILE
"$MEDIA_PY" "$MEDIA_HOME/wp_post.py" 投稿 "$@"
# Python側(wp_post.py)
KIJI_DIR = os.environ.get("MEDIA_KIJI_DIR", "")
LIST_FILE = os.environ.get("MEDIA_LIST", "")
Pythonのスクリプトを呼ぶときも "$MEDIA_HOME/wp_post.py" とフォルダ付きで書いています。場所を知っているのはシェル1か所だけ。Pythonは渡されたものを使うだけにすると、どこから起動されても同じ結果になります。
「見つからない」にはもう1種類ある
正直に書いておくと、うちが実際に朝の自動化を止めたのは、相対パスではなく権限のほうでした。相対パスで転ばずに済んだのは、最初から上の書き方をしていたからで、運がよかっただけです。
2026年8月27日の朝9時、記事作成の自動化がこのログを残して止まりました。
find: /Users/…/Desktop/新規事業/G_メディア事業/記事/wordpress投稿用: Operation not permitted
パスは1文字も間違っていません。手で叩けば普通に読めます。それでもlaunchdからは読めなかった。macOSがデスクトップ以下を保護していて、launchd経由のプログラムには許可していなかったからです。同じ日の10時49分に記事の置き場を ~/scripts/media/記事 に移して直しました。config.sh の1行を書き換えるだけで済んだのは、置き場所を設定1か所にまとめてあったおかげです。
つまり、症状がまったく同じ「手動では動くのにlaunchdでは見つからない」でも、原因は場所と権限の2種類あります。切り分けは簡単で、
- ログのメッセージが
No such file or directory系 → 場所の問題。この記事の話 Operation not permitted→ 権限の問題。パスは合っている
と読んでください。エラー文をそのまま検索する前に、この2語だけ見分ければ、探す方向を間違えずに済みます。
明日やるなら、この順番で
- launchdに登録してあるスクリプトを開き、
./で始まる行と、フォルダ名なしのファイル名を全部探す - まず
sourceの行を絶対パスに直す。ここが落ちると他が全部無意味になる - 置き場所が変わらないものは
$HOME/…、フォルダごと動かしうるものはBASE="${0:A:h}"に統一する - plistの中に
~が1つも無いか確かめる。特にStandardOutPathとStandardErrorPath - 直したら、時刻を待たずに1回手で動かして、ログに何も出ないことを確かめる
手で動かして通ったからといって、launchdで通る保証にはなりません。相対パスを全部消し終えて初めて、その2つが一致します。
まとめ
- launchdはスクリプトの置き場所からは起動しない。相対パスは手動テストだけ通って、本番で落ちる
WorkingDirectoryは任意のキー。書かなければ場所はこちらの都合では決まらない- 直し方は3つ。①$HOME起点の絶対パス ②
${0:A:h}で自分の場所を測る ③先頭でcd … || exit 1 - うちは③をplistではなくスクリプト側に置いている。手動でもlaunchdでも同じ場所から始まるようにするため
- plistの中の
~は展開されない。ログの出し先を~で書くと、失敗の理由すら残らない - 置き場所は設定1か所にまとめる。うちはそのおかげで、置き場の引っ越しが1行の書き換えで済んだ
- 同じ症状でも
No such file or directoryは場所、Operation not permittedは権限。先にログの2語を見る
Operation not permitted のほう、つまりパスは合っているのにmacOSの保護で読めないケースは launchdが動かない・終了コード127の原因と対処 に、うちが実際に踏んだ経緯ごとまとめてあります。ログにこの文字列が出ているなら、この記事ではなくそちらを読んでください。


コメント