手動ではSlackに届くのにlaunchdでは通知されない|環境変数が引き継がれない落とし穴

手動ではSlackに届くのにlaunchdでは通知されない|環境変数が引き継がれない落とし穴 定期実行と通知

ターミナルで手で動かせばSlackにちゃんと通知が届く。ところが同じスクリプトを、時間になったらMacが勝手に動かす仕組み(launchd)から呼ぶと、処理は最後まで終わっているのにSlackだけ届かない。エラーは1つも出ません。

結論を先に書きます。launchdは ~/.zshrc を読みません。ターミナルで export して用意した環境変数は、launchdから起動したスクリプトには1つも渡っていません。だから「環境変数からWebhook URLを読む」という作りにしていると、定期実行のときだけ中身が空になり、通知の部分だけが黙って飛ばされます。

私は清掃業を経営していて、プログラマーではありません。この記事は、うちの自動化で実際に起きたことと、そのあと採用した渡し方を、実物のログとスクリプトを見ながら書いています。

処理は「完了」しているのに、Slackに何も来ていない

うちの自動化のうち、記事を作って投稿するものの実行ログにこう残っていました。

毎日運転を開始: 2026-08-26 11:04:53
(中略)
投稿完了: 下書き
Slack通知保留: Incoming Webhookが未設定です
投稿完了: 下書き
Slack通知保留: Incoming Webhookが未設定です
毎日運転を完了: 2026-08-26 11:14:29

読んでのとおり、記事は作られ、WordPressへの下書き投稿も2件終わり、最後に「完了」と書かれています。終了コードも0(成功)です。失敗として扱われていないので、失敗通知も飛びません。届かなかったのは通知だけで、しかもその事実はログの中にしかありませんでした。

通知を出す部分は、こういう作りになっていました。

webhook = os.environ.get("TRAVEL_SLACK_WEBHOOK", "")
if not webhook.startswith("https://hooks.slack.com/"):
    print("Slack通知保留: Incoming Webhookが未設定です")
    return

環境変数が空なら、1行書き残して何事もなく戻る。処理そのものは止めない、という判断です。この判断自体は悪くありません。通知が出せないくらいで記事の投稿まで巻き添えにする必要はないからです。問題は、空になる条件が「launchdから動かしたとき」だったことでした。

なぜ環境変数が消えるのか

ターミナルを開くと、zshは ~/.zshrc を読み込みます。うちの ~/.zshrc にも、PATHの設定とAIサービスのAPIキーが export で書いてあります。だから手で動かしたときは、環境変数が全部そろった状態でスクリプトが走ります。

launchdはこの手順を踏みません。launchdがスクリプトに渡すのは、plist(launchdの設定ファイル)の EnvironmentVariables に書いたものだけです。うちには16個のplistが登録してあります。そのうち環境変数を書いてあるのは8個で、残りの8個には1行もありません。しかも書いてある8個も、中身は全部PATHの1項目だけでした。

<key>EnvironmentVariables</key>
<dict>
  <key>PATH</key>
  <string>/Users/自分の名前/.local/node/bin:/Users/自分の名前/.local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
</dict>

つまりPATH以外は何も渡っていない。手元では動くのに定期実行では動かない、という現象のかなりの部分はこれが原因です。

いま何が渡っているかを、その場で確かめる

推測せずに実際に見るのが早いです。スクリプトの先頭に、次の1行を一時的に足します。

env | sort > "$HOME/launchd_env_確認.txt"

そのうえで、手ではなくlaunchdに動かしてもらいます。ラベルの部分は自分のものに変えてください。

launchctl kickstart -k gui/$UID/com.media.generate

出てきた ~/launchd_env_確認.txt を開いて、探している名前の行があるかを見ます。無ければそれが答えです。確認が済んだら、足した1行は必ず消してください。

秘密情報をplistに書いて解決してはいけない

「渡らないなら、plistの EnvironmentVariables にWebhook URLを書けばいい」と考えたくなります。実際それで動きます。ただし、やめたほうがいいです。

  • plistはただのテキストで、中身がそのまま読めます
  • 置き場所が ~/Library/LaunchAgents なので、Macのバックアップに素直に乗ります
  • 設定を人に見せたり、AIに読ませたりする場面で、いちばん最初に開くファイルです

スクリプトの中に直接書くのも同じ理由でだめです。うちはスクリプトをAIに読ませて直させることがあるので、なおさら分けておく必要がありました。秘密情報は「動かすための設定」とは別の場所に置き、スクリプトが自分で取りに行く形にします。

うちで実際に使っている2つの渡し方

1. Macのキーチェーンに預ける

キーチェーンは、Macが最初から持っているパスワードの保管庫です。ここに入れておけば、ファイルとして裸で置いておく必要がなくなります。登録は1回だけです。

security add-generic-password -U -s travelmedia-slack-webhook -a "$USER" -w 'ここにWebhookのURL'

読み出しはこうです。

security find-generic-password -w -s travelmedia-slack-webhook

スクリプト側は、環境変数があればそれを使い、無ければキーチェーンから取る、という書き方にしてあります。実物はこの形です。

read_keychain() {
  /usr/bin/security find-generic-password -w -s "$1" 2>/dev/null || true
}

if [ -z "${TRAVEL_SLACK_WEBHOOK:-}" ]; then
  export TRAVEL_SLACK_WEBHOOK=$(read_keychain "travelmedia-slack-webhook")
fi

こう書くと、手で動かしてもlaunchdから動いても、同じように値が入ります。誰が起動したかで結果が変わらなくなる、というのがこの書き方の要点です。実際、この形にしてからの毎朝8時の自動実行のログを9月1日から10日まで見直したところ、「Incoming Webhookが未設定です」は1日も出ていませんでした。launchdから起動しても、値はちゃんと入っています。

ただし、通知が全部届いたわけではありません。9月3日と4日のログにはこう残っていました。

Slack通知失敗: <urlopen error [Errno 8] nodename nor servname provided, or not known>

これは「Slackのアドレスが引けなかった」、つまり朝8時の時点でネットにつながっていなかったということです。環境変数の問題とは別物で、直し方も違います。ここで大事なのは、失敗の中身がログに残っていたおかげで区別できたことです。「未設定です」なら渡し方の問題、「urlopen error」なら回線の問題。同じ「Slackに来ない」でも原因は別なので、まずログを見ます。

もうひとつ正直に書いておくと、キーチェーンを使うやり方はMacにログインしたままの状態で動かしている前提です。ログイン中はキーチェーンが開いているので読めます。ログアウトした状態でどうなるかは、うちでは試していないので書けません。

2. 本人だけが読めるファイルに1行だけ書く

キーチェーンより単純にしたいときは、URLだけを書いたファイルを置き、権限を本人だけに絞ります。事務自動化ラボ側の通知はこちらです。

printf '%s\n' 'ここにWebhookのURL' > ~/scripts/media/.slack_webhook
chmod 600 ~/scripts/media/.slack_webhook

chmod 600 は「自分だけが読み書きできる」という意味です。実際、うちのファイルは -rw------- になっています。読む側はこう書いています。

hook=$(head -1 "$HOME/scripts/media/.slack_webhook" | tr -d '[:space:]')
case "$hook" in
  https://hooks.slack.com/*) ;;
  *) return 1 ;;
esac

ここで「https://hooks.slack.com/ で始まっているか」を毎回確かめているのが地味に効きます。ファイルが空だったり、コピーのときに余計な文字が混ざったりしても、変な宛先に投げずに済みます。

どちらを選ぶか

キーチェーン 600のファイル
手軽さ 登録に1コマンド要る ファイルを置くだけ
中身の見えにくさ 裸のテキストでは残らない 権限で守るだけ
バックアップに乗ったとき そのままでは読めない 読めてしまう
止めたいとき キーチェーンから消す ファイル名を変えるだけ

うちは、外部サービスへの投稿権限そのものにあたるもの(WordPressのトークン、旅行メディアのWebhook)はキーチェーン、社内向けの通知先はファイル、と分けています。厳密なルールというより、事故ったときの被害の大きさで分けているだけです。

未設定のまま何日も気づかない、を防ぐ

通知の仕組みには構造的な弱点があります。「通知が来ないこと」は、通知では知らせられません。だからうちは、通知とは別に2つ置いています。

ひとつは、処理を始める前の確認です。旅行メディア側では、記事を作り始める前に必要な登録がそろっているかを見て、足りなければその場で言わせています。

if ! /usr/bin/security find-generic-password -s travelmedia-slack-webhook >/dev/null 2>&1; then
  echo "事前確認注意: Slack自動通知はまだ未接続です"
fi

もうひとつは、毎日13時に動く「見張り」です。その日の自動処理がひととおり動いたかを1件ずつ確かめて、Slackに1通だけまとめて出します。個々の通知が飛ばなくても、この1通が来なければ何かおかしい、と分かる作りにしてあります。

明日、10分で確かめられること

  1. plistに秘密情報が入っていないか見る。
    grep -l "hooks.slack.com\|xoxb-\|api_key" ~/Library/LaunchAgents/*.plist
    何も出てこなければ問題ありません。ファイル名が出たら、そこから外してキーチェーンかファイルに移します。
  2. 手ではなくlaunchdに1回動かしてもらう。
    launchctl kickstart -k gui/$UID/ラベル名
    手で動かして満足しないこと。この記事の失敗は、手で動かしている限り絶対に再現しません。
  3. 通知が本当に届くか、単体で試す。
    zsh -c 'source ~/scripts/media/通知.sh; shiraseru "テスト" "通知先の確認です" ok'
    Slackに届けば通知先は生きています。届かなければ、処理が正しくてもお知らせは来ません。

まとめ

  • launchdは ~/.zshrc を読まない。ターミナルで export した環境変数は1つも渡らない
  • 環境変数から秘密情報を読む作りだと、定期実行のときだけ空になる。しかもエラーにならず、終了コードは0のまま
  • 解決策としてplistに書き足すのはだめ。plistは平文で、バックアップにも人の目にも触れる
  • スクリプト自身に取りに行かせる。キーチェーン、または chmod 600 のファイル。「環境変数があればそれ、無ければ取りに行く」と書けば、誰が起動しても同じ結果になる
  • 読み込んだ値は必ず形を確かめる。https://hooks.slack.com/ で始まるか、の1行で事故が減る
  • 通知が来ないことは通知では気づけない。別口で「今日ぜんぶ動いたか」を見る仕組みを1つ持つ

自動化の失敗で本当に怖いのは、止まることではなく、止まったのに成功に見えることです。今回は通知だけが抜けましたが、抜けたのが集計や投稿だったら気づくのはもっと遅れていました。

launchdから動かしたときだけ挙動が変わる、という話はこれだけではありません。ファイルが見つからなくなる場合については Macでは動くのにlaunchdではファイルが見つからない|相対パスの落とし穴 に書きました。原因は違いますが、見つけ方の考え方は同じです。

コメント

タイトルとURLをコピーしました