- macOSの launchd にフォルダを見張らせて、ダウンロードした画像を自動で1か所に集める設定をしました。
- Downloads 側はフルディスクアクセスを与えると動きましたが、OneDrive 側だけが Resource deadlock avoided で失敗し続けました。
- 原因は launchd ではなく、OneDrive のファイル管理機構と、バックグラウンドのシェルコマンドの衝突でした。
- 最終的に、移動処理そのものを Finder に任せる形に変えて動くようになりました。
1. 自動でファイル整理したい
これまで、カメラロールやダウンロードフォルダに溜まった写真は、自分でスクリプトを実行して1か所に集めていました。 仕組みとしては足りていたのですが、ダウンロードするたびに手で実行するのが、だんだん面倒に感じるようになりました。 それなら、システム側に自動で動いてもらおう。 そう考えて設定を始めたら、思っていたより遠回りすることになりました。

最終的に必要なファイルは2つです。
移動処理を行う Automator アプリと、それを起動する LaunchAgent の plist です。
1.1. Automator アプリ(Move Images.app)
Automator で新規書類からアプリケーションを選び、「AppleScriptを実行」アクションにスクリプトを貼り付けて保存します。
保存先は ~/Applications/ です。
全ユーザー向けの /Applications に入れると、plist の指すパスと食い違います。
~/Applications/Move Images.app
Code language: JavaScript (javascript)
保存後に一度手動で起動し、Finder の操作を許可します。許可の状態はシステム設定のプライバシーとセキュリティ、オートメーションで確認できます。
1.2. LaunchAgentsのplist(com.local.move-images.plist)
~/Library/LaunchAgents/com.local.move-images.plist を作成します。Label はファイル名と揃え、ProgramArguments で /usr/bin/open にアプリのパスを渡します。
plist 内では ~ が展開されないため、/Users/ユーザー名/ から書きます。
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>/Users/ユーザー名/Applications/Move Images.app</string>
</array>
<key>WatchPaths</key>
<array>
<string>/Users/ユーザー名/Downloads</string>
<string>/Users/ユーザー名/OneDrive/画像/カメラ ロール</string>
</array>
<key>StartInterval</key>
<integer>30</integer>
<key>RunAtLoad</key>
<true/>
Code language: HTML, XML (xml)
作成後に、launchctrlコマンドで登録します。
PLIST="$HOME/Library/LaunchAgents/com.local.move-images.plist"
plutil -lint "$PLIST"
launchctl bootstrap "gui/$(id -u)" "$PLIST"
Code language: Bash (bash)
1.3. ログは自動で作られる(move-images-finder.log )
ログは AppleScript 側が ~/Library/Logs/move-images-finder.log に追記します。
plist に StandardOutPath と StandardErrorPath を書いた場合は、同じ ~/Library/Logs/ に launchd 側のログも出ます。移動先の ~/Pictures/inbox だけは、事前に mkdir -p で作っておきます。
2. launchd にフォルダを見張らせる

macOSには launchd(ランチディー)という常駐の仕組みがあり、アプリやスクリプトの起動を管理しています。
このうち LaunchAgent(ランチエージェント)は、ログインしているユーザーの権限で動くジョブです。 設定ファイルは plist という XML 形式のファイルで、~/Library/LaunchAgents/ に置きます1。
やりたいことは単純です。
-
~/Downloadsと OneDrive のカメラロールフォルダを見張って、 - PNG と JPEG が増えたら
~/Pictures/inboxに移動する。
移動用のスクリプトは zsh で書いて、~/.local/bin/move-images.zsh に置きました。
find "$watch_dir" -maxdepth 1 -type f \
\( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' \) \
-print0 |
while IFS= read -r -d '' file; do
/bin/mv -n "$file" "$DEST/"
done
Code language: JavaScript (javascript)
フォルダ内の変更を検知する設定が WatchPaths です2。
plist にはこう書きます。
<key>WatchPaths</key>
<array>
<string>/Users/ユーザー名/Downloads</string>
<string>/Users/ユーザー名/OneDrive/画像/カメラ ロール</string>
</array>
Code language: HTML, XML (xml)
plist の中では ~ が展開されないので、絶対パスで書く必要があります。
2.1. WatchPaths はポーリングではない
最初に気になったのは、どれくらいの頻度で見張っているのかという点でした。 決まった間隔で見に行っているなら、その間隔だけ移動が遅れることになります。
実際には定期的な確認ではなく、ファイルシステムの変更イベントを受けて起動する方式です。 何も起きていないときは、プロセスは動いていません。 変更があった瞬間に launchd がスクリプトを起動します。
ただ、短時間に何度も変更が起きると、そのたびに起動されても困ります。 そのために ThrottleInterval で最短の起動間隔を指定できます。 5 を指定すると、実行間隔はおおむね5秒以上に抑えられます3。
それでも、変更イベントを取りこぼす場合があります。 クラウド同期のように、こちらの操作とは関係なくファイルが増える経路では、いつ変化するか読めません。 そこで StartInterval を足して、30秒ごとにも起動するようにしました。 イベント検知と定期実行のどちらかが引っかかれば動く、という二重の構えです。
StartInterval はスリープ中には実行されず、復帰後にさかのぼって実行されることもありません4。 起きている間だけ30秒おきに確認する、と考えておけば十分です。
2.2. 動いているかどうかは launchctl print で読む
登録したあと、本当に監視されているのかを確認したくなります。 launchctl print にドメインとラベルを渡すと、そのジョブの現在の状態が出てきます5。
launchctl print "gui/$(id -u)/com.local.move-images"
Code language: Bash (bash)
出力の中で見るべき行は限られています。
state = not running
runs = 608
last exit code = 0
watching = 1
Code language: PHP (php)
watching = 1 なら、フォルダの変更を待っている状態です。 runs はこれまでの実行回数なので、時間をおいて2回見比べれば、起動しているかどうかが分かります。 state = not running は一見すると不安になりますが、イベント待ちのジョブは普段プロセスを持ちません。 止まっているのが正常な姿。
last exit code = 0 については、あとで少し裏切られました。 find の失敗はパイプの途中で起きるため、スクリプト全体の終了コードには表れません。 0 が並んでいても、中で毎回エラーが出ていることがあります。
3. 権限を通したら、別のエラーが出てきた
再起動したあと、監視は動いているのに画像が移動していませんでした。 runs は増えていて、watching も 1 のままです。 トリガーは生きている。

エラーログを見ると、同じ行が延々と並んでいました。
find: /Users/ユーザー名/OneDrive/画像/カメラ ロール: Operation not permitted
Code language: JavaScript (javascript)
macOSは、ダウンロード、書類、デスクトップなどへのアクセスをアプリごとに制御しています6。 ターミナルから自分で実行したときは、ターミナルに与えられた許可の範囲で動きます。 一方、launchd から起動された /bin/zsh は別のプロセスなので、その許可を引き継ぎません。
同じ Mac で、定期実行から Slack にメッセージを送る処理は動いていました。 ネットワークを使う処理は通るのに、ファイルを読む処理だけが拒否される。 許可の枠が用途ごとに分かれているせいです。
システム設定のプライバシーとセキュリティから、フルディスクアクセスに /bin/zsh を追加しました7。 これで Downloads 側は通るようになりました。 ところが OneDrive 側は、エラーの文面が変わっただけでした。
mv: rename ...: Resource deadlock avoided
Code language: HTTP (http)
3.1. Resource deadlock avoided は File Provider の反応だった

Resource deadlock avoided は、デッドロックになる手前でシステムが処理を打ち切ったときに返るエラーです。 ファイルの移動でこれが出るのは、素朴に考えると妙な話です。 権限の問題なら Permission denied のはずで、存在しないなら No such file のはず。
手がかりは、実行元による違いにありました。 ターミナルから同じスクリプトを走らせると成功します。 launchd から起動すると mv も cp -p も失敗します。
macOSは、クラウドストレージのファイルを File Provider という仕組みで扱います8。 OneDrive や iCloud Drive のフォルダは、通常のディスク上のフォルダのように見えて、実際にはこの仕組みが間に入って管理しています。 ファイルの実体をいつ持ってきて、いつ捨てるかを決めているのも、この層です。
複数のプロセスが同じファイルを同時に触るときの調整も、この層の担当になります。 Appleはそのための API として NSFileCoordinator を用意していて、読み取りや移動を安全に行う手順を定めています9。 シェルの mv は、その手順を踏みません。
3.2. 未ダウンロードのファイルではなかった
最初に疑ったのは、ファイルの実体がまだ手元にない状態でした。 OneDrive には Files On-Demand(ファイルオンデマンド)という機能があり、クラウド上にしかないファイルも、Finder には存在するように見せかけます10。 開こうとした時点で、初めて実体がダウンロードされます。
移動しようとした瞬間にダウンロードが始まり、その最中に移動も進もうとして噛み合わなくなる。 筋としては通っています。
ただ、対象のカメラロールフォルダは、以前の実験の段階で「このデバイス上で常に保持」に設定済みでした。 実体はすでにローカルにあります。 それでもエラーは出続けました。
つまり、待てば解決する種類の問題ではありませんでした。 実体があるかどうかではなく、誰がどの経路で触りに行くか。 そこで結果が変わっていました。
3.3. 同じ壁に当たった人たち
自分の環境固有の問題なのかを確かめたくて、同じエラーの事例を探しました。 似た話は、いくつも見つかりました。
いちばん近いのは、cron から cat でクラウド上のファイルを読もうとして Resource deadlock avoided になった例です。 その人はシェルでの処理をあきらめ、Automator の「Finder項目をコピー」アクションに置き換えて回避していました。 バックグラウンドのUNIXコマンドでは失敗し、Finder 経由では成功する。 今回とほとんど同じ構図です。
OneDrive に限っても報告があります。 Python のライブラリから OneDrive 内のファイルを読むと同じエラーになった例、バックアップソフトが読み込み時に EDEADLK を受け取り、一時的なエラーとして再試行の対象に加えた例1112。 Microsoft のフォーラムでも、クラウド専用ファイルが原因候補として挙げられ、「このデバイス上で常に保持」が対策として案内されています13。
対策の方向はばらばらですが、症状の出方は共通しています。 File Provider の管理下にあるファイルを、対話的でないプロセスがシェルコマンドで直接触ると、この壁に当たる。 既知の挙動だと分かった時点で、シェルで押し切る道は捨てました。
4. Finder に移動を頼む
方針を変えて、移動の実行者を Finder にすることにしました。 Finder は File Provider と正しくやり取りできる立場にあります。 launchd は起動のきっかけだけを担当し、実際のファイル操作は Finder に依頼する形です。

4.1. Automator アプリにして、オートメーションを許可する
Finder への指示は AppleScript で書きます。 拡張子を見て PNG と JPEG だけを選び、同名のファイルがあれば名前を変えてから移動する、という内容です。

tell application "Finder"
set destinationFolder to (POSIX file destinationPath) as alias
set candidateFiles to every file of sourceFolder
repeat with candidateFile in candidateFiles
move candidateFile to destinationFolder
end repeat
end tell
Code language: AppleScript (applescript)
これを launchd から osascript で直接呼ぶこともできます。 ただ、その場合は「何が Finder を操作しようとしているのか」の主体がはっきりせず、macOSのオートメーション権限で弾かれることがあります14。 Automator で独立したアプリとして保存すれば、そのアプリ単体に Finder の制御を許可できます。
Automator の新規書類でアプリケーションを選び、「AppleScriptを実行」アクションにコードを貼って保存しました15。 最初に手動で起動すると、Finder の操作を許可するかどうかを聞かれます。 許可したあとで実行すると、ログに移動の記録が並びました。
16:20:22 開始
16:20:53 移動: 20260723_025918974_iOS.jpg
16:20:55 移動: 20260723_025924667_iOS.jpg
16:21:00 終了
Code language: CSS (css)
5枚の移動に38秒。 シェルの mv に比べれば遅いのですが、OneDrive 側の調整を待ちながら動いていると考えれば、こんなものだと思います。
4.2. 最後に残ったのは、保存場所の間違い
あとは plist の起動対象を、スクリプトから Automator アプリに差し替えるだけです。
<key>ProgramArguments</key>
<array>
<string>/usr/bin/open</string>
<string>/Users/ユーザー名/Applications/Move Images.app</string>
</array>
Code language: HTML, XML (xml)
ところが、launchctl kickstart で起動しても、ログは空のままでした。 手動で起動すれば動くのに、launchd からだと何も記録されない。
open がアプリの終了を待たずに戻ってしまうせいかと考えて、終了を待つ -W と、新しいインスタンスで起動する -n を足しました16。 変わりません。 逆に、手動実行と条件を揃えるためにオプションを全部外しました。 これも変わりません。
原因は、アプリの置き場所でした。 Automator の保存時に、全ユーザー向けの /Applications に入れていたのです17。 plist が指していたのは /Users/ユーザー名/Applications のほう。 存在しないパスを開こうとしていたので、当然何も起きません。
アプリを ~/Applications に移したら、そのまま動きました。
権限の枠組みを調べ、クラウドストレージの内部構造を追い、同じ症状の事例を漁った末に、最後に残っていたのがパスの取り違えでした。 File Provider の話はどれも本当に必要な回り道でしたが、それはそれとして、動かないときはまず指しているパスを見ろ、ということでもあります。 自動化は、手でやっていた作業を機械に渡すだけの作業に見えて、実際には「手でやるときには意識しなくてよかった条件」を全部書き出す作業でした。 毎朝、写真が勝手に集まっているのを見ると、その手間の分は返ってきた気がします。
4.3. 不要になったもの(スクリプト)
シェル版の ~/.local/bin/move-images.zsh は、Finder 経由に切り替えた時点で使いません。
フルディスクアクセスに追加した /bin/zsh も、他の用途がなければ外して構いません。
- Appleは、ユーザーごとのエージェントは ~/Library/LaunchAgents に、システム全体のデーモンは /Library/LaunchDaemons に配置すると説明しています。 – Creating Launch Daemons and Agents
- WatchPaths に指定したパスに変更があるとジョブが起動します。ファイル単位でも指定できますが、そのファイルが削除されると監視は解除されます。 – launchd.plist(5)
- ThrottleInterval の既定値は10秒です。指定した秒数より短い間隔での再起動は抑制されます。 – launchd.plist(5)
- スリープ中に実行時刻を過ぎた場合、復帰後に一度だけ実行され、経過した回数分がまとめて実行されることはありません。 – launchd.plist(5)
- launchctl print は、指定したドメインまたはサービスの状態を表示するサブコマンドです。 – launchctl(1)
- Appleは、書類、ダウンロード、デスクトップなどのフォルダへのアクセスにユーザーの同意を求める仕組みを説明しています。 – macOSでのアプリからファイルへのアクセスの制御
- フルディスクアクセスは、プライバシーとセキュリティの設定画面でアプリ単位に許可します。 – Macでファイルとフォルダへのアクセスを制御する
- File Provider は、リモートに保管されたファイルをシステムに統合し、ローカルコピーの管理やダウンロードとアップロードを担当します。 – File Provider
- NSFileCoordinator は、複数のプロセスが同じファイルを読み書きする際の順序を調整するためのクラスです。 – NSFileCoordinator
- Files On-Demand では、ファイルを開いた時点で内容がダウンロードされます。常に手元に置く設定も選べます。 – Save disk space with OneDrive Files On-Demand for Mac
- 表計算ライブラリの利用者から、OSのアップグレード後にOneDriveとSharePoint上のファイルで Resource deadlock avoided が出るようになったと報告されています。 – macOS OneDrive/Sharepoint: Resource deadlock avoided issue since OS upgrade
- バックアップソフト側では、macOSで返る EDEADLK を一時的なエラーとして扱い、再試行する対応が議論されています。 – [BUG] Need to handle OSError EDEADLK/BlockingIOError on MacOS
- 回答では、クラウド専用の状態が原因になりうる点と、常に保持する設定への変更が案内されています。 – How to save to Onedrive? – Microsoft Q&A
- ほかのアプリを操作するには、操作する側のアプリごとに許可が必要です。 – Macのほかのアプリの制御を許可する
- Automator の「AppleScriptを実行」アクションは、ワークフローの中で任意のスクリプトを実行します。 – Use scripts with Automator on Mac
- -W は開いたアプリケーションが終了するまで待ち、-n はすでに起動していても新しいインスタンスを開きます。 – open(1)
- Appleは、すべてのユーザーが使うアプリは /Applications に、そのユーザーだけが使うアプリはホームフォルダの Applications に置くと説明しています。 – File System Basics