4 つの AI ブログを Cowork のスケジュールタスクで全自動運用し始めてから半年が過ぎた頃、こんなエラーがタスクログに突然現れました。
fatal: could not create work tree dir '/tmp/repos/claudelab.net': Permission denied
/tmp は誰でも書き込める一時領域のはずです。ディスク残量も 1,600 MB 以上ありました。なのになぜ Permission denied なのか。原因を調べると、/tmp/repos/ ディレクトリそのものが nobody:nogroup 所有になっており、現在のセッションユーザーには書き込み権限がなかったのです。
2014 年に個人開発を本格的に始めて以来、自動化スクリプトで繰り返し気づいてきたのは「成功を前提に書かれたコードは、必ず想定外の状況でつまずく」ということです。AdMob 収益が月間ピークに達していた時期でも、壁紙アプリのリリース自動化パイプラインでは環境依存のエラーが朝の配信を静かに止めていたことがありました。廣川が個人開発を通じて学んだこの種の問題の診断と修正の手順をまとめます。
なぜ /tmp/repos が nobody 所有になるのか
Cowork の VM 環境では、セッションをまたいで /tmp/ 以下のサブディレクトリが前のセッション由来の所有者のまま残ることがあります。前のセッションが nobody:nogroup として /tmp/repos/ を作成していた場合、次のセッションでは別のユーザーコンテキストで動くため、そのディレクトリへの書き込みができません。
仕組みを少し詳しく説明すると、/tmp/ 直下は drwxrwxrwt(スティッキービット付き、全ユーザーが書き込み可)ですが、その下に作られた /tmp/repos/ というサブディレクトリは通常の rwxr-xr-x 権限で作成されます。つまりサブディレクトリの所有者だけが書き込めます。mkdir -p /tmp/repos/claudelab.net がエラーなく通っても、中に git clone しようとすると弾かれる——というわかりにくい状況が生まれるのはこのためです。
# 所有者の確認
ls -la /tmp/repos/
# drwxr-xr-x 3 nobody nogroup 4096 ... ← これが問題のケース
# drwxr-xr-x 3 exciting-friendly-einstein ... ← これなら OKこの状況はセッションを新たに起動し直すとリセットされることもありますが、スケジュールタスクの文脈では「失敗するたびに手動で対処する」という運用は成立しません。コードで自動的に検知し、迂回する仕組みが必要です。
この問題が発生しやすいシナリオ
nobody:nogroup 問題が実際に起きやすいのは、次の 3 つの場面です。
1. Cowork をアップデートした直後
Cowork のバージョンアップやセッションの再起動によって、VM の内部ユーザーコンテキストが切り替わることがあります。前のセッションで作成された /tmp/repos/ は古いコンテキストの所有者のまま残るため、新しいセッションから見ると書き込み不可になります。
2. 複数のスケジュールタスクが同じ /tmp/repos/ を使っているとき
別の自動タスクが先に /tmp/repos/ を nobody:nogroup として作成してしまうと、後から実行されるタスクはその影響を受けます。4 サイト×複数タスクという構成で運用していると、タスク間の干渉がこの形で現れることがあります。
3. タスクがエラーで途中終了した後
前回のタスクが git 操作の途中で異常終了した場合、/tmp/repos/ 以下に不完全な状態のディレクトリが残ります。所有者の情報がその時点のコンテキストに紐付いているため、次の実行時に権限エラーが発生します。
これらのシナリオに共通するのは「前回の実行状態が今回に影響する」という点です。スケジュールタスクは毎回クリーンな状態で起動しているように見えますが、/tmp/ 以下のファイルシステムはセッションをまたいで共有されています。この前提を踏まえた設計が、安定した自動化の基本になります。
対処法:$HOME/repos へのフォールバック
最も確実な対処は、/tmp/repos/ が使えない場合に $HOME/repos/ へ自動的に切り替えることです。
# /tmp/repos が書き込み可能かチェックしてフォールバック
if [ -d /tmp/repos ] && [ ! -w /tmp/repos ]; then
echo "⚠️ /tmp/repos が書き込み不可(nobody 所有の可能性)— $HOME/repos に変更"
WORK="$HOME/repos/claudelab.net"
elif [ -w /tmp ] && mkdir -p /tmp/repos 2>/dev/null && [ -w /tmp/repos ]; then
WORK="/tmp/repos/claudelab.net"
else
WORK="$HOME/repos/claudelab.net"
fi
mkdir -p "$(dirname "$WORK")"
echo "📁 作業ディレクトリ: $WORK"$HOME は Cowork の VM では /sessions/セッション名/ 以下に展開され、現在のセッションが所有しているため確実に書き込めます。/tmp よりも永続性があるため、次回のスケジュール実行時に git pull --rebase で差分更新する「永続リポジトリ方式」にも向いています。
ディスク不足(ENOSPC)との組み合わせ診断
Permission denied と ENOSPC(No space left on device)はどちらも git clone を止めますが、原因と対処が異なります。どちらのエラーかは git clone が吐くメッセージで区別できます。Permission denied が見えれば所有者問題、No space left on device が見えればディスク不足です。スケジュールタスクの冒頭で両方を一度に診断し、それぞれに適した対処を自動で行う実装例を示します。
WS="$(ls -d /sessions/*/mnt/Dolice\ Labs 2>/dev/null | head -1)"
GITHUB_TOKEN=$(grep -A1 "Claude Lab" "${WS}/_documents/_github_tokens/github_tokens.txt" \
| tail -1 | tr -d '[:space:]')
# --- 1. /tmp/repos の書き込み可否チェック ---
if [ -d /tmp/repos ] && [ ! -w /tmp/repos ]; then
echo "⚠️ /tmp/repos が nobody 所有 — $HOME/repos に変更"
WORK="$HOME/repos/claudelab.net"
else
WORK="/tmp/repos/claudelab.net"
mkdir -p /tmp/repos 2>/dev/null || WORK="$HOME/repos/claudelab.net"
fi
# --- 2. ディスク残量チェック ---
FREE_MB=$(df "$(dirname "$WORK")" --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
echo "📊 残量: ${FREE_MB}MB(作業先: $WORK)"
if [ "${FREE_MB:-0}" -lt 300 ]; then
echo "⚠️ 残量不足 — 古いリポジトリを自動削除"
find "$(dirname "$WORK")" -maxdepth 1 -type d \
-not -name "$(basename $WORK)" \
-exec rm -rf {} + 2>/dev/null
FREE_MB=$(df "$(dirname "$WORK")" --output=avail -m 2>/dev/null | tail -1 | tr -d ' ')
echo "📊 削除後残量: ${FREE_MB}MB"
fi
if [ "${FREE_MB:-0}" -lt 200 ]; then
echo "⛔ それでも残量不足 — タスクを中止"
exit 1
fi
# --- 3. clone または pull ---
if [ -d "$WORK/.git" ]; then
cd "$WORK"
git remote set-url origin "https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git"
git pull --rebase origin main || {
rm -rf "$WORK"
git clone --depth 1 "https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git" "$WORK"
cd "$WORK"
}
else
git clone --depth 1 "https://${GITHUB_TOKEN}@github.com/masakihirokawa/claudelab.net.git" "$WORK"
cd "$WORK"
fi
git config user.email "masakihirokawa@gmail.com"
git config user.name "masakihirokawa"
echo "✅ リポジトリ準備完了"/tmp vs $HOME — Cowork VM での使い分け指針
どちらを使うべきかは、用途と実行頻度によって変わります。
単発で実行するスクリプトや、セッションをまたいで継続する必要がない処理には /tmp が手軽です。ただし nobody 問題が発生しうるため、書き込み可否チェックは必ず入れてください。
毎日複数回実行するスケジュールタスクには $HOME/repos/ が安定しています。リポジトリを使い捨てにせず git pull --rebase で差分更新する「永続リポジトリ方式」と組み合わせると、clone にかかる時間が不要になり、タスク全体の実行時間が大幅に短縮できます。個人開発で累計 5,000 万 DL を超えるアプリ群のリリース自動化を回しながら 4 つの AI ブログも並行して運用するようになった経験でいうと、このくらいの「作業場所を固定する」という設計が最もメンテナンスコストを下げます。
実際に Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab の 4 サイトを 1 日 4 本ずつ自動投稿する体制では、$HOME/repos/ を使った永続リポジトリ方式が唯一安定して動く方法でした。各リポジトリは node_modules なしで約 30 MB、4 サイト合計で 120 MB 程度を常時保持しています。
よくある関連エラーとその見分け方
同じ git clone の失敗でも、エラーメッセージで原因を素早く切り分けられます。
Permission denied — ディレクトリの所有者問題。ls -la でオーナーを確認し、nobody nogroup なら $HOME/repos/ へ切り替えます。
No space left on device — ディスク不足。df -h で確認し、古いリポジトリや .next/ キャッシュを削除します。
Could not resolve host — ネットワーク問題。VM のインターネットアクセス設定を確認します。
Repository not found — トークン期限切れまたはリポジトリ名変更。GitHub PAT を再生成して SKILL.md を更新します。
index.lock: File exists — 前の git プロセスが異常終了。.git/index.lock を削除するか、リポジトリを再 clone します。
SKILL.md への組み込み方
スケジュールタスクを SKILL.md で管理している場合は、Step 0 のリポジトリ準備部分の先頭に次のフォールバック検出を追加しておくと、nobody 問題が再発しても自動で回避できます。
# Step 0 の冒頭に追加するフォールバック検出(4 行)
_WORK_BASE="/tmp/repos"
if [ -d "$_WORK_BASE" ] && [ ! -w "$_WORK_BASE" ]; then
_WORK_BASE="$HOME/repos"
fi
WORK="${_WORK_BASE}/claudelab.net"
mkdir -p "$_WORK_BASE" 2>/dev/null || WORK="$HOME/repos/claudelab.net"SKILL.md を更新したら、スケジュールタスク本体のプロンプトにも同じ変更を反映させることを忘れないでください。両方を同期しないと、次回の実行タイミングで古い挙動に戻ります。これは手動実行ではわかりにくい罠で、スケジュールタスクが独立したプロンプトを読み込んで動作するという設計上、SKILL.md だけ直しても不十分です。
次にとるべきアクション
ls -la /tmp/repos/ を実行して所有者を確認してください。nobody nogroup が見えたら、SKILL.md の Step 0 に上記のフォールバックパターン(4 行)を追加するだけで解決します。ディスク不足が原因なら、ENOSPC 診断ブロックをクローン処理の前に入れてください。どちらの修正も 5 分以内に完了します。
4 サイト全自動投稿を安定稼働させるために、この種の「環境依存バグ」を一つひとつ潰してきました。個人開発を通じて培ったこの防御的な実装パターンが、スケジュールタスクで同じ問題に直面している方の参考になれば幸いです。自動化は「何も起きていないとき」ではなく「何かが起きたとき」に真価を問われます。