Claude Code の 2.1.248 の変更一覧を上から順に読んでいて、途中の一行で手が止まりました。Claude Desktop と Cowork のセッションが30日で消えていた、という修正です。
個人開発でいくつかのアプリを運営している関係で、私は毎日いくつかの処理を定期タスクに任せています。画像の書き出しやバックアップのようなもので、翌朝や週末に「あのとき何が起きていたか」を遡ることがあります。そのとき開いていたのは、アプリに並んでいるセッションの履歴でした。
言われてみると、一ヶ月より前のセッションを開いた記憶がありません。消えていることに気づいていなかったのではなく、消えているという可能性を考えたことがなかった、という感覚に近いです。
何が起きていて、何が変わったのか
公開されている変更履歴には、トランスクリプトの整理処理がデスクトップ側で書かれたセッションまで片付けていた、と書かれています。修正後は「アプリ内にある間は保持する」形になりました。ただし、組織のポリシーが保持期間を管理している場合はそちらが優先されます。
あわせて、例外期間に上限を設けるための desktopSessionCleanupPeriodDays という設定が新しく加わりました。
| 項目 | 修正前(〜v2.1.247) | 修正後(v2.1.248〜) |
|---|---|---|
| Desktop / Cowork のセッション | 整理処理の対象になり、30日で消えていた | アプリ内にある間は保持される |
| 組織ポリシーで保持を管理している場合 | — | ポリシー側が優先される |
| 例外期間の上限 | 設定なし | desktopSessionCleanupPeriodDays で指定できる |
同じリリースには、しばらく電源を落としていたマシンで数週間前の背景セッションが実行中として復活してしまう問題や、保存されなかったセッションの VSCode タブが「No conversation found」のまま固まる問題の修正も入っています。セッションの寿命と状態の扱いが、まとめて手当てされた回と読むのが正確だと思います。
大事なのは、この修正が過去に遡って効くものではないことです。すでに整理されてしまったセッションが戻ってくるわけではありません。今日から先の分が守られるようになった、という話です。
自分の環境が該当するかを確かめる
最初にやることは、手元のバージョンを見ることだけです。
claude --version2.1.248 以上であれば、この修正は入っています。翌日に出た 2.1.250 は安定化のみのリリースなので、そちらまで上がっていれば十分です。導入方法(npm・ネイティブバイナリ・各種パッケージマネージャ)によって更新の手順は変わるので、普段自分が使っている経路で上げるのが確実です。
デスクトップアプリ側の使い方そのものを整理したい場合は、Claude デスクトップアプリの機能を通しで見た記事のほうが向いています。ここでは、消えていた記録の話に絞ります。
desktopSessionCleanupPeriodDays は「保持する日数」ではありません
名前だけ見ると保存期間の設定に見えますが、変更履歴の書き方は「例外に上限を設ける(caps the exemption)」です。整理処理から外す扱いを無期限にせず、天井を与えるためのものだと読めます。
{
"desktopSessionCleanupPeriodDays": 180
}設定ファイルに書いたあとは、実際に読み込まれているかを確かめてください。Claude Code の設定は、キー名を一文字間違えても警告が出ず、そのまま無視されます。この挙動については設定キーの打ち間違いが黙って無視される話で詳しく扱っています。書いたのに効かない、という状態がいちばん厄介です。
私自身は、この値を大きくする方向にはあまり関心が向きませんでした。理由は次の節で書きます。
手元にいま何日分のセッションが残っているかを数える
まず現状を数えます。Claude Code はセッションのトランスクリプトを JSONL として保存しているので、本数と最古の経過日数を出すだけの短いスクリプトで足ります。
#!/usr/bin/env bash
# 手元に残っているセッション記録の本数と、いちばん古いものの経過日数を数える
set -euo pipefail
ROOT="${1:-$HOME/.claude/projects}"
if [ ! -d "$ROOT" ]; then
echo "not found: $ROOT"
exit 1
fi
NOW=$(date +%s)
TOTAL=0; OVER30=0; OLDEST=0
while IFS= read -r f; do
M=$(date -r "$f" +%s 2>/dev/null || stat -c %Y "$f")
AGE=$(( (NOW - M) / 86400 ))
TOTAL=$((TOTAL + 1))
[ "$AGE" -gt 30 ] && OVER30=$((OVER30 + 1))
[ "$AGE" -gt "$OLDEST" ] && OLDEST=$AGE
done < <(find "$ROOT" -type f -name '*.jsonl')
echo "root : $ROOT"
echo "sessions : $TOTAL"
echo "over 30d : $OVER30"
echo "oldest : ${OLDEST}d"動作を確かめるために、日付を散らしたサンプル5本を用意して走らせた結果がこちらです。
root : /home/me/lab/fake/projects
sessions : 5
over 30d : 2
oldest : 45d
保存先のパスは環境によって違います。上の既定値で見つからないときは、次のように探すと当たります。
find "$HOME" -name '*.jsonl' -path '*claude*' 2>/dev/null | headover 30d が 0 のまま増えていかないなら、整理処理に持っていかれている可能性があります。逆にここが素直に伸びていくなら、手元は保持側に回っています。数字が一つあるだけで、「消えているかもしれない」という曖昧な不安が、確かめられる事実に変わります。
セッションを記録の置き場にしない
ここからが、私が設定値を大きくする方向へ進まなかった理由です。
セッションのトランスクリプトは、会話の全部が入っている代わりに、後から探すには重すぎます。定期タスクで知りたいのは「昨日の夜の処理は通ったのか、それとも見送られたのか」であって、そこに至る全文ではありません。全文が残っていることと、必要なことが読めることは別です。
そこで、実行のたびに1行だけ、セッションとは別の場所に書くようにしました。
#!/usr/bin/env bash
# 定期実行の結果を、セッションとは別の場所へ必ず1行残す
JOURNAL_DIR="${JOURNAL_DIR:-$HOME/run-journal}"
JOB="${1:?job name required}"; shift
mkdir -p "$JOURNAL_DIR"
JOURNAL="$JOURNAL_DIR/$(TZ=Asia/Tokyo date +%Y-%m).log"
STATUS="FAILED" # 何も起きなければ失敗として残す
NOTE=""
write_line() {
printf '%s\t%s\t%s\t%s\n' \
"$(TZ=Asia/Tokyo date '+%Y-%m-%d %H:%M:%S')" "$JOB" "$STATUS" "$NOTE" >> "$JOURNAL"
}
trap write_line EXIT # 途中で落ちても1行は残る
if OUT=$("$@" 2>&1); then
STATUS="SUCCESS"; NOTE=$(printf '%s' "$OUT" | tail -1 | cut -c1-120)
else
CODE=$?
if [ "$CODE" -eq 2 ]; then STATUS="SKIPPED"; else STATUS="FAILED"; fi
NOTE="exit=$CODE $(printf '%s' "$OUT" | tail -1 | cut -c1-100)"
fi設計の要点は三つです。
STATUS の初期値を FAILED にしてあります。うまくいったときにだけ成功で上書きする形なので、途中で落ちた実行が「記録なし」として消えることがありません。何も書かれていない行がないので、あとから数えたときの合計がずれません。
trap write_line EXIT を先に張ってあります。これがないと、強制終了された実行だけが痕跡を残さずに消えます。いちばん知りたい失敗が、いちばん残りにくい状態になるわけです。
終了コード 2 を「見送り」に割り当てています。前回の処理がまだ走っている、対象が0件だった、といった正当な理由で何もしなかった実行を、失敗と同じ扱いにしないためです。
四通りの終わり方を実際に流してみました。
./run_journal.sh backup-photos bash -c 'echo "copied 128 files"'
./run_journal.sh thumbnail-batch bash -c 'echo "already running"; exit 2'
./run_journal.sh sync-remote bash -c 'echo "ssh: connection refused" >&2; exit 255'
./run_journal.sh crash-case bash -c 'kill -9 $$'出力された記録です。
2026-08-29 15:08:32 backup-photos SUCCESS copied 128 files
2026-08-29 15:08:32 thumbnail-batch SKIPPED exit=2 already running
2026-08-29 15:08:32 sync-remote FAILED exit=255 ssh: connection refused
2026-08-29 15:08:32 crash-case FAILED exit=137
最後の行に注目していただきたいです。kill -9 で落とした実行にも exit=137 として1行が残っています。トラップを張っていなければ、この行だけが存在しません。
月の集計は一行で出ます。
awk -F'\t' '{c[$3]++} END {for (s in c) printf "%-8s %d\n", s, c[s]}' "$HOME"/run-journal/*.logFAILED 2
SUCCESS 1
SKIPPED 1
見送りを成功に混ぜていないので、この数字は素直に読めます。処理を任せる範囲が広がるほど、成功・失敗・見送りを別々に数えられることの価値は上がっていきます。別セッションに長い処理を預けて待つ構成でも同じ考え方が要るので、そちらは完了マーカーで待ち合わせる記事にまとめてあります。
なお、ロックの取得に失敗した実行を「見送り」として残す設計は、置き場所によっては素直に動きません。クラウド同期のフォルダのように削除が拒まれる保管先では、ロックがそのまま残って二度目以降を締め出します。この落とし穴は連携フォルダのロックが解除できなくなる話で実測とともに扱いました。
今日やる一つのこと
claude --version を打って、2.1.248 以上かどうかだけ確かめてください。上がっていればセッションは守られる側に回っていますし、上がっていなければ、いま見えている履歴は消える前提で扱うことになります。
そのうえで、今月分の記録用ファイルを一つ作っておくと、来月には比べられるものが手元にあります。私も、この一件をきっかけにようやくそこへ手をつけました。お読みいただきありがとうございました。