水曜の夕方、定期タスクのログを開くと、最初の Git 操作で止まっていました。
fatal: could not create work tree dir '/tmp/repos/claudelab.net': Permission denied
手順書には「永続リポジトリを /tmp/repos に置いて、次回は git pull する」と書いてあります。この一行は、その前提が成り立たない日があることを教えてくれました。
この記事は、その原因の切り分けと、「他の実行が残したクローンを借りる」のをやめて「毎回 clone し直す」と決めた判断の記録です。Cowork の定期タスクで Git のリポジトリを触っている方に、同じ場所で足を取られない材料をお渡しできればと思います。
切り分けの四つのコマンド — /tmp は書けて /tmp/repos は書けません
最初に疑ったのは /tmp そのものでした。ディスクの空きか、書き込み禁止か。ここを順に確かめました。
id # 自分は誰か
ls -ld /tmp /tmp/repos # 親と子の所有者・モード
mkdir /tmp/repos/x # 子の下に作れるか
touch /tmp/_w_test && echo "tmp root writable" # /tmp 直下には書けるか
実際の出力はこうでした(抜粋)。
uid=1079(rcw-017tvrnbapgi3k7hq52tm5wv) gid=1079(...) groups=1079(...)
drwxrwxrwt 15 nobody nogroup 4096 Oct 21 08:44 /tmp
drwxr-xr-x 3 nobody nogroup 4096 Oct 21 01:16 /tmp/repos
mkdir: cannot create directory '/tmp/repos/x': Permission denied
tmp root writable
読み取れることは三つです。/tmp は drwxrwxrwt(sticky ビット付きで誰でも書ける)なので、直下への touch は通ります。一方 /tmp/repos は所有者が nobody、モードが 755 で、私のユーザー(uid 1079)は「入れるが、中に作れない」。そして /tmp/repos の作成時刻は同じ日の 01:16 で、別の実行が先に作ったものでした。
/tmp が書けることと、/tmp の下にある誰かのディレクトリが書けることは別の話です。 私はここで「/tmp は書けるのだから、問題は別にある」と一度考えてしまい、先にディスク容量の確認から入りました。空きは / で 3,787 MB、/sessions で 7,229 MB あり、原因ではありませんでした。
借りられる古いクローンはありました — ただ約9.6時間遅れていました
/tmp の直下には、別の実行が残したと思われる claudelab.net のクローンがありました(所有者は同じく nobody)。読み取りはできます。「これをコピーして使えば、clone の手間が省ける」という考えが浮かびました。
そこで、使う前に中身の新しさだけ確かめました。
git -c safe.directory='*' -C /tmp/claudelab.net log -1 --format='%h %ci'
git -C "$HOME/repos/claudelab.net" log -1 --format='%h %ci' # 手元で取得した最新
9296076 2026-10-20 23:06:43 +0000 ← 借りられる側
3dd2738 2026-10-21 08:41:15 +0000 ← origin/main の最新
借りられるクローンは、origin の最新より約9.6時間遅れでした。しかも .git に書けないため(touch .git/probe が Permission denied)、その場で git pull もできません。コピーして使うなら、コピーした後に pull する二段構えになります。
これは、原稿の追記をする作業にとっては静かに危険な状態です。9時間ぶんの更新を知らないまま slug の重複確認や内部リンクの存在確認をすると、「ない」と判断した記事が実際にはある、あるいはその逆が起きます。古いクローンを借りることの本当のコストは、clone の時間ではなく、判断を誤る確率です。
毎回 clone し直した場合の費用
では、使い回しをやめた場合にいくらかかるのか。数えました。
使い回しで節約できるのは、1 回あたり約2 秒です。1 日 1 回の定期タスクなら、1 年で約12 分。これと引き換えに「他の実行の状態に依存する」「古い内容に気づけない」を抱えるのは、私の使い方では割に合いませんでした。
注意点が一つあります。$HOME は実行ごとに名前の違う場所(今回は /sessions/rcw-…)で、次の実行には引き継がれませんでした。つまり「$HOME/repos に置いて次回 pull」は期待できず、毎回 clone になります。それでも 2.8 秒です。
作業場所を選ぶスクリプトと、clone 後の照合
方針は二つに絞りました。書ける場所を、実際に子を作って確かめてから選ぶこと。 そして作業を始める前に、手元の HEAD と origin/main の SHA を突き合わせること。
最初のスクリプトは作業場所の選択です。
#!/usr/bin/env bash
# pick_base.sh — 書き込める作業ディレクトリの親を先頭から順に探して返す
set -u
CANDIDATES=("/tmp/repos" "${TMPDIR:-/nonexistent}/repos" "$HOME/repos")
for base in "${CANDIDATES[@]}"; do
# mkdir -p は「既にあるが書けない」場合も成功してしまう。
# だから実際に子を作って消し、書けることを確かめる
mkdir -p "$base" 2>/dev/null || { echo "skip(mkdir): $base" >&2; continue; }
probe="$base/.probe.$$"
if mkdir "$probe" 2>/dev/null; then
rmdir "$probe"
echo "$base"
exit 0
fi
echo "skip(owner=$(stat -c %U "$base") mode=$(stat -c %a "$base")): $base" >&2
done
echo "FATAL: 書き込める作業場所がありません" >&2
exit 1
この環境で走らせると、次のように /tmp/repos を理由つきで飛ばし、TMPDIR 配下を選びました。
skip(owner=nobody mode=755): /tmp/repos
/sessions/rcw-017tvrnbapgi3k7hq52tm5wv/tmp/repos
なぜ mkdir -p だけで済ませないのか。mkdir -p は既存のディレクトリに対して成功を返すので、書けない /tmp/repos を「使える」と誤判定してしまうためです。実際に今回、skip(mkdir) ではなく skip(owner=…) の行が出たことで、この挙動を確認しました。
二つ目は、clone と照合です。
#!/usr/bin/env bash
# fresh_repo.sh <remote-url> <name> — 作業ディレクトリのパスを標準出力へ
set -euo pipefail
REMOTE="$1"; NAME="$2"
BASE=$("$(dirname "$0")/pick_base.sh")
DIR="$BASE/$NAME"
if [ -d "$DIR/.git" ]; then
# 既存クローンを借りるのは「自分が書けて、かつ最新」のときだけ
if [ -w "$DIR/.git" ] && git -C "$DIR" pull -q --rebase origin main 2>/dev/null; then
:
else
echo "stale or read-only: $DIR → 作り直します" >&2
rm -rf "$DIR" 2>/dev/null || DIR="$BASE/$NAME.$$"
fi
fi
[ -d "$DIR/.git" ] || git clone -q --depth 1 --branch main "$REMOTE" "$DIR"
# 最後に必ず確かめる: 手元の HEAD とリモートの main が同じ SHA か
LOCAL=$(git -C "$DIR" rev-parse HEAD)
REMOTE_SHA=$(git -C "$DIR" ls-remote origin main | cut -f1)
[ "$LOCAL" = "$REMOTE_SHA" ] || { echo "FATAL: HEAD=$LOCAL origin/main=$REMOTE_SHA" >&2; exit 2; }
echo "$DIR"
照合を最後に置いたのは、「借りない」と決めても、pull が無言で失敗する経路(認証切れ、ネットワーク断)が残るからです。2>/dev/null で握りつぶしている以上、終了コードだけでは古いまま進んでしまいます。SHA が一致するまで作業ディレクトリを返さないようにしておけば、古い状態で書き始めることはありません。
定期タスクの冒頭に置く三つの手順
二つのスクリプトを、手順書の最初にこの順で置きました。
pick_base.sh で書ける場所を決めます。失敗したらここで止め、後続の手順には進みません。
fresh_repo.sh を呼び、返ってきたパスを変数に入れます。SHA が一致するまでパスは返りません。
- 以降の手順では、その変数だけを使います。
/tmp/claudelab.net のような固定パスを手順書の本文に残さないことを推奨します。
私の場合、一番効くと見ているのは三つ目です。固定パスが一か所でも残っていると、場所探しを整えたそばから、古い手順がそのパスを読みに行ってしまうからです。
落とし穴
rm -rf /tmp/repos/* でのお掃除は、他の実行が作ったものには効きません。 削除は対象ではなく親ディレクトリの書き込み権限で決まるため、今回のように nobody 所有の 755 の下では消せません。容量不足の対処手順に「/tmp を空ける」と書いてある場合は、自分が作った場所だけを消す形に直しておく必要があります。
- 他人所有のクローンを
git で読むときは、所有者の確認で止められることがあります。 今回は -c safe.directory='*' をその呼び出し限りで付けて読みました。設定ファイルへグローバルに書き込むのは避けています。
- トークンを URL に埋め込む方式は、この記事のスクリプトでは引数の
REMOTE に任せています。 認証の持たせ方は別の問題で、リダイレクト後に通らなくなった経緯は別の記事にまとめました。ログにトークンが出ないよう、私は出力を sed で伏せてから確認しています。
- 作業場所の取り違えは、そもそも別のシェルを見ている場合にも起きます。
WS が空になった朝の話はこちらに書いています。今回の件とは原因が別ですが、「最初に確かめる一行」という対処の考え方は共通です。
使い回しをやめてよい条件と、やめない条件
私の判断は「リポジトリが 50 MB 程度で、実行が 1 日数回まで」という前提つきです。条件が変われば、結論も変わります。
- clone が数十秒以上かかるほど大きい(履歴を深く取る、LFS を使うなど)なら、使い回しの価値が出ます。その場合も「自分が書ける場所に置く」「SHA を照合する」の二点は残してください。
- 毎回 clone するなら、
--depth 1 を外さないことが前提です。履歴まで取ると 2.8 秒の話ではなくなります。
- 実行が並列に走るなら、
$BASE/$NAME.$$ のように実行ごとに名前を分けると、取り合いは避けられます(上のスクリプトでは、削除に失敗したときだけこの形に落としています)。
次にやることは一つです。定期タスクの最初の手順に、pick_base.sh と SHA の照合を置いてみてください。置いた日から、「今日の作業場所はどこか」を毎回考えずに済みます。