Syncthing 使い方:競合ファイルと.stignore
同期フォルダーを開いたら、notes.md の隣に notes.sync-conflict-20260925-060144-US3TWHL.md ができていた。素直に考えれば、この競合コピーを書いたのはデバイス US3TWHL だと思うはずです。私のテストでは逆でした。名前に入っている ID は、自分の版が勝ったデバイスのもので、コピーの中身は負けたデバイスの編集でした。
競合コピーと .stignore は、Syncthing の動きが予想といちばん食い違いやすい二か所です。そこで Syncthing のインスタンスを二つ用意し、八つのケースをわざと起こしました。両側での同時編集を向きを変えて二通り、編集と削除のぶつかり合い、競合コピーを無効にした場合、そして無視パターンの四つの状況です。以下の結果はすべて、テストログと両側のディレクトリ一覧から取ったものです。
結論(Syncthing v2.1.5、2026-09-25 に検証):
| 状況 | 起きたこと |
|---|---|
| 接続が切れている間に、両方のデバイスで同じファイルを編集した | 更新日時が新しいほうの編集が両側で勝った。もう一方の編集は name.sync-conflict-<date>-<time>-<ID>.ext として両方のデバイスに保存された |
競合ファイル名の <ID> | 自分の版が勝ったデバイスの短い ID。コピーの中身を編集したデバイスではない |
競合ファイル名の <date>-<time> | Syncthing が競合を処理した時刻で、誰かがファイルを編集した時刻ではない。コピー自体は負けた側の元の更新日時を保っている |
| 片方が削除し、もう片方が編集した | 編集が勝った。削除した側にファイルが戻り、競合コピーは作られなかった |
両方のデバイスで maxConflicts を 0 にした | 負けた編集は破棄された。どちらの側にも競合コピーは残らない |
片方のデバイスにだけ .stignore を書いた | 同期されない。パターンはそのデバイスでしか効かなかった |
#include でパターンを共有した | 動いた。読み込まれるファイルは、ほかのファイルと同じように同期される |
| 無視されたファイルが入ったフォルダーを、別のデバイスが削除した | フォルダーは削除されず、同期フォルダーは未同期(Out of Sync)になって「失敗した項目」が一つ出た。パターンに (?d) を付けると解消した |
| すでに同期済みのフォルダーを無視対象にした | 中の新しいファイルが同期されなくなり、中のファイルの削除も同期されなくなった。相手側には古いコピーが残った |
公式ドキュメントに書いてあること
同期のページ(v2.1.0 向けに書かれたもの、2026-09-24 に確認)は、名前の形式を <filename>.sync-conflict-<date>-<time>-<modifiedBy>.<ext> と説明し、次のように書いています(訳は筆者)。
更新日時が古いほうのファイルが競合ファイルとして扱われ、名前が変更されます。
<modifiedBy> が誰の ID なのかは書かれていません。そこで v2.1.5 のソース(lib/model/folder_sendrecv.go)を確認しました。ローカルのファイルが競合で名前を変えられるとき、接尾辞は受信したファイルの ModifiedBy フィールドから取られます。これは勝った版です。タイムスタンプは、名前を変えるその瞬間に time.Now() で取られます。私のテストはどちらとも一致しました。
無視のページ(v2.1.0 向けに書かれたもの、2026-09-24 に確認)には、.stignore ファイルは「ほかのデバイスに同期されることは決してないが、デバイス間で同期されるファイルを #include することはできる」とあり、(?d) 接頭辞は「そのファイルがディレクトリの削除を妨げている場合に、それらを削除できるようにする」とあります。設定のページは maxConflicts を「任意のファイルについて保持しておく競合コピーの最大数。デフォルトは 10」と説明し、0 にすると「競合コピーを完全に無効にする」と書いています。ただ、0 にすると負けた編集がそのまま消えてしまうことには触れていません。私のテストではそうなりました。
検証環境
- 一台の Mac 上に Syncthing v2.1.5 のインスタンスを二つ(
inst1とinst2)。公式の darwin-arm64 ビルドを使用。どちらも 127.0.0.1 でだけ待ち受け、「グローバル探索」「LAN内で探索」、中継サーバー経由の通信、NATトラバーサルはすべてオフ。 - 両者で共有する新しいフォルダー
t4を一つ。両側とも送受信(Send & Receive)、ファイルのバージョン管理はオフ、maxConflictsはデフォルトの 10。 - 「変更の監視」(Watch for Changes)はオフにし、再スキャンはすべて手動で実行しました。イベントの順番が監視のタイミングに左右されないようにするためです。
- 本物の競合を起こすため、二つのデバイス間の接続を一時停止し、両側で同じファイルを編集して再スキャンし、それから一時停止を解除しました。
- ここまでの操作はすべて、REST API を通して Python スクリプトで行いました。スクリプトと完全なログは私自身の検証メモで、以下の結果はそのログから引用しています。
inst1 の短い ID は V3OZYEV で始まり、inst2 は US3TWHL で始まります。
テスト1:競合でどちらが勝つか、ファイル名は何を意味するか
ケース A。 接続が切れている間に、inst1 は caseA.md に「edited on inst1」と書き、更新日時は 05:59:39 でした。inst2 は「edited on inst2」と書き、更新日時は 06:00:39、つまり一分後です。06:01:42 に再接続しました。数秒後、両方のデバイスはこうなっていました。
caseA.md "edited on inst2" mtime 06:00:39
caseA.sync-conflict-20260925-060144-US3TWHL.md "edited on inst1" mtime 05:59:39
新しいほうの編集(inst2)が勝ちました。競合コピーの中身は inst1 のテキストなのに、名前の末尾は US3TWHL、つまり inst2 です。
ケース B はその逆で、今回は inst1 の編集のほうが新しくなっています。
caseB.md "edited on inst1" mtime 06:00:52
caseB.sync-conflict-20260925-060204-V3OZYEV.md "edited on inst2" mtime 05:59:52
ここでも接尾辞は勝った側(inst1、V3OZYEV)で、中身は負けた側のものです。
ここから分かることは三つです。
- 競合ファイル名の ID は「勝ったデバイス」と読む。 負けたテキストを誰が書いたかを知りたければ、もう一方のデバイスを見てください。
- 名前に入る時刻は競合が処理された時刻(06:01:44、再接続の二秒後)で、誰かが何かを入力した時刻ではありません。コピー自身の更新日時には、元の編集時刻(05:59:39)がそのまま残っています。
- 競合コピーは普通のファイルなので、同期されます。 最終的に両方のデバイスにありました。ドキュメントによれば、これは意図した動きです。その競合は「ほかのどこでも同じく競合」だからです。
テスト2:片側で編集、もう片側で削除
ケース C。 接続が切れている間に、inst1 は caseC.md を削除し、inst2 はそれを編集しました。再接続後、両方のデバイスに inst2 が編集した内容の caseC.md があり、競合コピーはどこにもありませんでした。
つまりこのケースでは編集が勝ち、削除したデバイスにファイルが戻ってきました。消したはずのファイルが復活したら、オフラインの間にほかのデバイスがそれを変更していなかったか確認してください。
テスト3:maxConflicts = 0
ケース D。 REST の設定 API で両方のデバイスの maxConflicts を 0 にし(maxConflicts はフォルダーごとの設定です)、ケース A を繰り返しました。今回も inst2 の編集のほうが新しくなっています。
inst1: caseD.md "edited on inst2"
inst2: caseD.md "edited on inst2"
それだけです。inst1 のテキスト「edited on inst1」は、どちらのデバイスのどのファイルにもありませんでした。ファイルのバージョン管理はオフだったので、ほかに残しているものもありません。
maxConflicts = 0 は競合を隠す設定ではありません。古いほうの編集が捨てられるという意味です。競合ファイルが煩わしいなら、中身を確認してから削除するか、0 ではなく 1 のような小さい数にしてください。
テスト4:.stignore は書いたデバイスにしか効かない
ケース E。 inst1 の .stignore にだけ *.log と書き、inst1 で e1.log、inst2 で e2.log を作りました。
inst1: caseE.md e1.log
inst2: caseE.md e2.log
.stignore 自体は inst2 に届かず、.log ファイルはどちらの方向にも移動しませんでした。inst2 には無視パターンが無いのに、inst1 は e2.log を受け取りませんでした。無視パターンは、該当するファイルの送信と受信の両方を止めます。
テスト5:#include でパターンを共有する
ケース F。 inst1 で、*.bak と書いた .stglobalignore を作りました。普通のファイルなので inst2 に同期されました(存在を確認済み)。そのうえで次のようにしました。
inst1 .stignore: *.log
#include .stglobalignore
inst2 .stignore: #include .stglobalignore
GET /rest/db/ignores が返した実際に効いているパターンは、inst1 で *.log と *.bak、inst2 で *.bak でした。inst1 で作った f1.bak は inst1 に、inst2 で作った f2.bak は inst2 に留まりました。
これはドキュメントが勧めているやり方です。どこでも効かせたいルールは同期されるファイルに書き、各デバイスの .stignore からそれを #include します。デバイス固有のルールは .stignore 自体に残します。ドキュメントには、読み込むファイルが存在しないとエラーになるとも書かれているので、#include の行を足す前に、共有ファイルを作って同期させておいてください。
テスト6:無視されたファイルがフォルダーの削除を止める、(?d) で直る
フォルダーが止まったまま動かなくなるのは、このケースです。
ケース G。 gdir と hdir という二つのフォルダーに、それぞれ普通のファイルを一つずつ入れて、両方のデバイスに同期しました。inst2 で、それぞれに無視されるファイルを一つずつ置きました。gdir/junk(素の junk パターンで無視)と hdir/junk2((?d)junk2 で無視)です。それから inst1 で両方のフォルダーを削除しました。
- inst2 では
hdirが削除された。junk2も一緒に消えた。 gdirは inst2 に残り、中身はjunkだけになった。
inst2 のフォルダーは未同期(Out of Sync)に変わり、「失敗した項目」が一つ出ました。

失敗した項目(Failed Items)の件数をクリックすると、理由がはっきり書かれています。日本語表示にしていても、エラー文そのものは英語のままです。

inst1 側では、リモートデバイス inst2 のこのフォルダーの完了率が 95% のまま止まっていました。テストの残りの時間(その後約五分)、自然に解消することはありませんでした。
次に inst2 のパターンを junk から (?d)junk に変えて再スキャンしました。一分もしないうちに gdir は削除され、失敗した項目は消え、inst1 から見た inst2 は再び 100% になりました。つまり、すでに止まっているフォルダーも (?d) を足せば直せます。最初からやり直す必要はありません。ドキュメントが (?d) を勧めているのは、.DS_Store のような OS が勝手に作るファイルです。大事なファイルを守るためのパターンには (?d) を付けないでください。付けると、そのファイルはフォルダーと一緒に削除されます。
テスト7:すでに同期済みのフォルダーを無視する
ケース H。 hold/old.md は両方のデバイスに同期済みでした。その後 inst1 で .stignore に hold/ を足し、hold/new.md を作り、さらに後で hold/old.md を削除しました。
inst2 hold/ after new.md was created on inst1: old.md
inst2 hold/ after old.md was deleted on inst1: old.md
new.md が届かなかったのは想定どおりです。分かりにくいのは次の点です。削除も同期されませんでした。 パスが無視対象になると、Syncthing はそのパスについて削除も含めて一切の変更を送らなくなります。そのため、相手のデバイスには手元にあったコピーがそのまま残ります。フォルダーをほかのデバイスから消し、しかもその後は無視したいなら、まず削除し、削除が同期されるのを待ってから、無視パターンを足してください。
私ならこうする
- 競合ファイルを見つけたら、本体のファイルと見比べて手でマージし、それから競合ファイルを削除する。 接尾辞の ID は勝ったデバイスで、中身は負けた編集です。
maxConflictsを 0 にしない。 二つの編集のうち古いほうを失っても構わないと確信できるなら別です。もう一段の安全策が欲しければ、ファイルのバージョン管理をオンにしてください。- 共有する無視ルールは同期されるファイルに書き(例えば
.stglobalignore)、すべてのデバイスでそれを#includeする。.stignoreだけでは、それが置かれたデバイスしかカバーできません。 - OS が作るゴミファイルには
(?d)を使う。 そうすれば、削除したフォルダーがほかのデバイスで止まらなくなります。すでに「contains ignored files」という失敗した項目が出ているフォルダーでも、そのパターンに(?d)を足して再スキャンしたら、私のテストでは解消しました。 - フォルダーをすべての場所から消し、その後も入れたくないときは、先に削除、無視はその後。
この検証で扱っていないこと
- 検証したのは macOS 上の Syncthing v2.1.5 だけで、一台のマシン上の二つのインスタンスを使い、2026-09-25 に行いました。大文字と小文字を区別しないファイルシステムや、ほかの OS は試していません。
- フォルダーはすべて送受信でした。受信のみ、送信のみのフォルダーでは、ローカルの変更の扱いが違います。
- 競合は各ケースで一回ずつ、二つの編集の間隔は一分で試しました。更新日時がまったく同じ編集は試していません。ドキュメントによれば、その場合はデバイス ID で決まります。
- 編集と削除のケースでは編集が勝ちました。削除が勝つケースは見つからなかったので、ドキュメントにある「削除が勝つ」ときの競合コピーは見ていません。
- ファイルのバージョン管理は最初から最後までオフでした。