Cowork の自動タスクで、こんな矛盾に出くわしたことはないでしょうか。スケジュール実行のログには cat: settings.json: No such file or directory と出ているのに、同じファイルを Finder で開くと中身がちゃんと表示される。フォルダを接続したはずなのに、bash からだけファイルが「無い」ことになっている——この食い違いは、環境の不具合ではなく、クラウド同期フォルダの仕組みがそのまま表面化したものです。
私自身、個人開発の自動化を Cowork のスケジュールタスクに任せるようになってから、この「見えているのに読めない」現象を何度か踏みました。原因が分かってしまえば対処は単純ですが、知らないと「タスクが無音で失敗する」という一番やっかいな形で現れます。ここでは、なぜ起きるのかを腑分けしたうえで、自動化が取りこぼさないための設計パターンまで落とし込みます。私はこの順番——入力源と作業領域を分け、触れてから処理する——を守るようになってから、無人タスクの不発がほぼなくなりました。
クラウド同期は「実体のないファイル」を置く
Dropbox・iCloud Drive・OneDrive などのクラウド同期は、ディスク容量を節約するために「オンデマンド実体化(on-demand materialization)」という仕組みを持っています。フォルダの一覧には全ファイルが並んでいるように見えますが、実体(中身のバイト列)はクラウド側にしかなく、ローカルにはメタデータだけのプレースホルダが置かれている状態が普通にあります。
ここで Cowork の構造が効いてきます。Cowork の bash はサンドボックス化された Linux 環境で、接続フォルダをマウント経由で参照します。bash が見ているのは「ディスク上に実体としてあるバイト列」だけです。プレースホルダはディレクトリ一覧(ls)には現れますが、cat で中身を読もうとした瞬間に実体がないので失敗します。一方、Cowork の file ツール(Read など)は、読み取り要求をきっかけにクラウドからの実体ダウンロードを引き起こします。つまり同じパスでも、bash は実体を要求できず、file ツールは要求できる という非対称があるのです。
「Finder には見えるのに bash には無い」の正体はこれです。Finder(や同期クライアント)はプレースホルダを開いた瞬間にダウンロードを走らせますが、bash の cat はそのトリガーを持っていません。
症状を3つに切り分ける
「読めない」とひとくくりにせず、まず実体の状態を3つに分けて観測すると、対処が決まります。
F = "/sessions/xxx/mnt/Workspace/settings.json"
# 1. 一覧には出るか(メタデータの有無)
ls -la " $F " # 出る = プレースホルダは存在
# 2. 実体のバイト数はあるか
stat -c '%s bytes' " $F " 2> /dev/null || echo "stat failed"
# 3. 実際に読めるか
head -c 16 " $F " 2>&1 | head -1
観測される典型は次の3パターンです。
状態 ls stat のサイズ cat / head 意味
実体なし 出る 失敗 or 0 No such file / I/O error 未ダウンロードのプレースホルダ
0 バイト 出る 0 空が返る 同期途中・実体化失敗
古い実体 出る 正の値 読めるが内容が古い 別端末の更新が未反映
3つ目の「古い実体」が一番こわい症状です。cat は成功し、エラーも出ないのに、内容が最新ではない。自動化はエラーがないと「成功した」と判断してそのまま進むので、古いデータで動いた結果が静かに積み上がります。件数チェックや整合性チェックを後段に置くべき理由はここにあります。
bash のパスと file ツールのパスは別物
もう一つ、自動化を組むうえで前提にすべき事実があります。bash が使うパスと、file ツールが使うパスは同じファイルでも異なる ということです。Cowork では、接続フォルダ・スクラッチの作業領域・読み取り専用のアップロードなどが、それぞれ別のマウントポイントとして bash 側に現れます。
そのため、自動化スクリプトの先頭で「いま自分はどのパスにいるのか」を動的に検出してから始めるのが安全です。セッション名をハードコードすると、次回の実行で別セッションになった瞬間に全パスが外れます。
# セッション名をハードコードしない。マウントを動的に拾う
WS = "$( ls -d /sessions/ * /mnt/Workspace 2> /dev/null | head -1 )"
[ -z " $WS " ] && { echo "⚠️ ワークスペース未検出" ; exit 1 ; }
echo "WS= $WS "
この一行があるだけで、「昨日は動いたのに今日は全部パスエラー」という最頻出のトラブルをまるごと避けられます。私はスクリプトの Step 0 に必ずこれを置くようにしています。
設計パターン①:触れてから処理する
ここからが本題です。bash が実体を要求できない以上、処理の前に「実体化を済ませておく」のが基本戦略になります。Cowork で対話的に作業できる場合は、file ツールで対象ファイルを一度読む(=ダウンロードを誘発する)だけで、その後の bash が同じパスを問題なく読めるようになります。
スケジュールタスクのように bash しか使えない文脈では、別の手が要ります。同期クライアントに実体化を促す副作用を持つ操作を、エラーを握りつぶしながら一度通すのです。
# 実体化トリガ(クラウド側に実体がある前提で、まず一読を試みる)
warm_file () {
local f = " $1 "
# 失敗してもいいので軽く触る。同期クライアントによっては
# この read 要求がダウンロードのきっかけになる
cat " $f " > /dev/null 2>&1 || true
# 少し待ってから実体の有無を判定
for i in 1 2 3 4 5 ; do
local sz; sz = $( stat -c '%s' " $f " 2> /dev/null || echo 0 )
[ "${ sz :- 0 }" -gt 0 ] && return 0
sleep 2
done
return 1
}
if warm_file " $WS /settings.json" ; then
echo "✅ 実体化OK"
else
echo "⚠️ 実体化できず: $WS /settings.json(クラウド未ダウンロード)"
fi
ポイントは、実体化が成功したことを stat のバイト数で確認してから次に進む ことです。cat の終了コードだけを見ると、0 バイトの空ファイルでも「成功」になってしまいます。
設計パターン②:スクラッチへの一方向コピー
クラウド同期フォルダを直接の作業対象にすると、読みの不確実性に加えて、書き込みの競合(別端末の同期と衝突)まで抱え込みます。自動化を安定させたいなら、同期フォルダは入力源としてだけ使い、実処理はスクラッチ領域で行う のが堅いです。
次の Before / After は、私が自動化スクリプトで実際に切り替えた形です。
Before(同期フォルダ上で直接処理・不安定):
# 同期フォルダ内で生成物を組み立てる
cd " $WS /project"
python3 build.py # 入力が未実体化だと無音で空出力
git -C " $WS /project" commit -am "update" # 同期と衝突しやすい
After(スクラッチへコピーしてから処理・安定):
SCRATCH = " $HOME /work/project" # 同期外のローカル作業領域
mkdir -p " $SCRATCH "
# 入力を実体化しつつ一方向コピー(rsync は実体を要求する)
rsync -a --no-perms " $WS /project/inputs/" " $SCRATCH /inputs/" 2>&1 | tail -2
cd " $SCRATCH "
python3 build.py # 実体のある入力で確実に動く
# 生成物だけを最後に同期フォルダへ書き戻す
rsync -a " $SCRATCH /dist/" " $WS /project/dist/"
rsync は中身をコピーするために実体を要求するので、コピー自体が実体化のトリガになります。さらに作業を同期外に逃がすことで、ビルド中に同期クライアントが割り込んでファイルを書き換える事故も防げます。ディスクが厳しい環境では、処理後に rm -rf "$SCRATCH" で畳めば容量も圧迫しません。実際、私の手元では入力が約 1,200 ファイルに達する日でも、先にスクラッチへ一括コピーしてから処理することで、未実体化による空出力を 0 件に抑えられています。同期フォルダを直接いじるより、ワンクッション挟むこの方式を私はおすすめします。
どの読み方を選ぶか
ここまでの選択肢を、状況別に整理します。万能の正解はなく、実行文脈(対話か無人か)と、対象の性質(小さい設定ファイルか、大量の素材か)で使い分けます。
状況 推奨 理由
対話セッションで個別ファイルを読む file ツールで一度開く ダウンロードを自動で誘発でき、確実
無人タスクで小さな設定を読む warm_file で実体化を確認 bash だけで完結し、stat で空を排除できる
大量ファイルを処理する スクラッチへ rsync コピー 実体化と競合回避を同時に満たせる
大量・大容量を一括取得する前 何を落とすか先に明示する 意図しない大量ダウンロードを避ける
生成物を書き戻す 最後にまとめて一方向コピー 処理中の同期衝突を断つ
最後の「大量取得の前に明示する」は運用上のマナーでもあります。クラウド同期フォルダに対して無差別に find ... -exec cat を走らせると、フォルダ全体のダウンロードを誘発して、回線も同期クライアントも巻き込みます。私は対象を絞り込み、何件・どのくらいの容量を落とすのかをログに出してから実体化するようにしています。
取りこぼしを件数とハッシュで検知する
「古い実体を黙って読んでしまった」という最悪のパターンは、処理の前後に軽い検証を挟むだけでかなり防げます。入力の件数とハッシュを取っておき、想定とズレたら止めるのです。
# 入力の健全性チェック(空・件数ゼロを早期に弾く)
N = $( find " $SCRATCH /inputs" -type f -name '*.json' | wc -l )
[ " $N " -eq 0 ] && { echo "⚠️ 入力ゼロ件。実体化失敗の可能性" ; exit 1 ; }
echo "inputs= $N 件"
# 重要ファイルは内容ハッシュで「空でない・壊れていない」を確認
H = $( sha256sum " $SCRATCH /inputs/manifest.json" | cut -c1-12 )
echo "manifest sha= $H "
スケジュールタスクはエラーが出ない限り「成功」として記録されてしまうので、exit 1 で意図的に落とす条件を持っておくことが、無音の失敗を可視化する一番の近道です。エラーで止まったタスクは気づけますが、空のまま完走したタスクには誰も気づけません。
安定化を3手順で組み込む
ここまでの要点は、次の3手順に畳めます。新しい自動化を書くたびに、この順で組み込むことをおすすめします。
検出 :スクリプト冒頭で WS="$(ls -d /sessions/*/mnt/... | head -1)" を実行し、マウントを動的に拾う。空なら即 exit 1。
実体化 :入力を warm_file または rsync でスクラッチへ落とし、stat のバイト数が正であることを確認してから処理に入る。
検証 :処理前に入力件数([ "$N" -eq 0 ] && exit 1)、重要ファイルは sha256sum で「空でない・壊れていない」を確認する。
この3手順を雛形として持っておくと、サイトや題材が変わっても土台が崩れません。
次の一歩
まずは自分の自動化スクリプトの先頭に、ワークスペースの動的検出と入力件数チェックの2つを足してみてください。WS="$(ls -d /sessions/*/mnt/... | head -1)" と、処理直前の [ "$N" -eq 0 ] && exit 1 を置くだけで、「見えているのに読めない」と「古いまま完走した」の両方を早期に捕まえられるようになります。クラウド同期は便利な反面、実体化のタイミングをこちらが握れていないと自動化の足元が崩れます。入力源と作業領域を分けて、触れてから処理する——この順番を守るだけで、無人運用の安定感は大きく変わります。
同じように個人開発の自動化を Cowork に預けている方の、つまずきを一つ減らせたなら嬉しいです。