夕方、書き出したばかりの設定ファイルを開こうとして、手が止まりました。Claude の返事には「ファイルを作成しました」とあります。ところがフォルダを見ても、ターミナルで ls を打っても、そのファイルはどこにもありません。
その日は、個人開発している壁紙アプリのストア用メタデータを整える作業の途中でした。私自身、Claude に「このフォルダに JSON を書き出して」と頼むのは日常の手順になっています。
エラーは一行も出ていません。だからこそ、胃のあたりが少し冷たくなりました。失敗したと分かっていれば、やり直せば済みます。厄介なのは、成功したと信じたまま次の工程へ進んでしまうことのほうです。
「作成しました」の応答と実体がずれたときの切り分けの順番と、書き込みの直後に読み戻して確かめる小さな検証を、ここに書き残します。
症状の見分け方 — 「作らない」ではなく「残らない」
最初にお伝えしたいのは、この症状は二つに分けて考えると迷わない、ということです。
| 症状 | よくある原因 | 最初に確かめること |
|---|---|---|
| 応答は成功、別の場所には存在する | 書き込み先のパスが想定とずれている | 応答が示した絶対パス、作業ディレクトリ |
| 応答は成功、どこにも存在しない | 書き込みが反映されていない(権限、同期フォルダ、接続の状態) | 同じパスを読み戻した結果(ENOENT か否か) |
| 既存ファイルの編集は通る、新規作成だけ消える | 新規作成の経路だけが別の挙動をしている | 新規作成と既存編集を別々に試す |
コミュニティの報告には、ファイルシステム系の MCP サーバーで新規作成だけが成功応答のまま残らない、という事例があります。既存ファイルの編集と読み取りは安定しているのに、新規作成だけが消える——しかもアプリを再起動しても同じ、という内容です。クラウド同期のマウント配下で報告されている点も気になります。
私はこの報告を、原因の断定には使いません。同じ形の症状が実際に報告されているという手がかりとして読み、自分の環境で切り分けます。
原因を切り分ける順番
順番を決めておくと、慌てずに済みます。
- 応答に書かれたパスを、そのままコピーして
ls -lに渡す。 - 見つからなければ、親ディレクトリを
ls -laで見る。ディレクトリ自体が無いのか、ファイルだけが無いのか。 - 同じ操作を、同期されていないローカルフォルダ(たとえば
/tmp配下)で試す。 - 既存ファイルへの追記や編集が通るかを見る。
- 接続しているツールの一覧と状態を確認する。切れていれば、つなぎ直す。
3 番が要です。同期フォルダでだけ起きるなら、原因の候補は「書き込み経路」から「同期の層」へ絞れます。ローカルでも起きるなら、ツール側の問題として扱います。
直し方 — 書き込みの直後に読み戻す
切り分けより前に、被害を小さくする仕組みを一つ置いています。書いたら、必ず読み戻します。 応答の文言ではなく、ファイルの実体を見るということです。
次のスクリプトは、期待するファイルが存在し、空でなく、中身が一致するかを確かめます。何を解決するかといえば、「作成しました」という言葉を信じる代わりに、実体で次の工程へ進む可否を決めることです。
#!/usr/bin/env bash
# verify_written.sh — 書き込み直後の読み戻し検証
# 使い方: ./verify_written.sh <パス> [期待する文字列]
set -u
path="$1"
expect="${2:-}"
if [ ! -e "$path" ]; then
echo "NG: 存在しません: $path" >&2
ls -la "$(dirname "$path")" >&2 2>/dev/null || echo "NG: 親ディレクトリも見つかりません" >&2
exit 1
fi
if [ ! -s "$path" ]; then
echo "NG: 空ファイルです: $path" >&2
exit 2
fi
if [ -n "$expect" ] && ! grep -qF -- "$expect" "$path"; then
echo "NG: 期待した内容が含まれていません: $expect" >&2
exit 3
fi
echo "OK: $(wc -c < "$path") bytes — $path"なぜこう書くのかというと、終了コードを三種類に分けておくと、呼び出し側が「存在しない」「空」「内容違い」を区別できるからです。自動で回す処理でも、どこで止まったかがログに残ります。
Claude Code から使うなら、書き込みの直後に呼ぶ形が自然です。
# 例: 設定ファイルを作らせた直後の確認
./verify_written.sh ./config/app.settings.json '"version"' || echo "再作成が必要です"Python で組み込む場合は、次の形になります。
from pathlib import Path
def verify_written(path: str, expect: str = "") -> None:
"""書いたはずのファイルを実体で確かめる。失敗なら例外で止める。"""
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"書き込みが残っていません: {p}")
text = p.read_text(encoding="utf-8")
if not text.strip():
raise ValueError(f"空ファイルです: {p}")
if expect and expect not in text:
raise ValueError(f"期待した内容がありません: {expect!r}")
verify_written("./config/app.settings.json", '"version"')なぜ応答の言葉だけでは足りないのか
応答は「ツールを呼んだ」という事実と、「ツールが成功を返した」という事実を伝えています。ただし、そのあとにファイルが残ったかどうかまでは、応答のどこにも書かれていません。
途中には、ツールの実装、OS の権限、同期クライアントといった層が挟まっています。どこか一層でも書き込みを取りこぼせば、上流は何も知らないまま「成功」を返してしまうのです。だから確かめる場所は、いちばん下流にある実体、つまりディスク上のファイルになります。
もう一つ、読み戻しは安上がりです。1 回あたりの手間はほんの数十ミリ秒で、見逃したときの手戻りに比べればはるかに小さな保険だと感じています。
直らないときの次の一手
読み戻しが失敗し、再作成しても同じなら、次の三つを順に試します。
- 書き込み先を同期の外へ移す。 いったんローカルに作り、完成してから同期フォルダへコピーします。これで通るなら、同期の層が原因の可能性が高まります。
- 新規作成をやめ、既存ファイルを編集する形にする。 空のテンプレートを先に手で置き、編集で中身を埋めます。報告にあった「編集は安定している」という観察に沿った、いわば回り道です。
- 別の経路で書く。 ツール経由が不安定なら、Claude にシェルのコマンドで書かせ、同じ検証を通します。
どれも根治ではありません。それでも、検証が付いていれば、仕事が止まる前に気づけます。
私が決めた線引き
最初のうちは、応答を信じすぎていました。「作成しました」と言われれば、そのまま次の手順へ進んでいたのです。結果は芳しくありませんでした。存在しないファイルを前提にした処理が、あとから別のエラーとして現れ、原因を辿るのに余計な時間がかかりました。
いまは、こう決めています。言葉は参考に、実体は自分で。 書き込みの応答を受け取ったら、それが何であれ、読み戻して初めて「できた」と数えます。
まずは、いま使っている手順のうち、ファイルを新しく作らせる箇所を一つだけ選んで、直後に verify_written.sh を置いていただければと思います。私もそこから始めました。