.gitkeepとは?Gitで空ディレクトリを管理する方法と運用上の落とし穴
結論
.gitkeepは、空のディレクトリをGitで追跡するためのダミーファイル。Gitはファイル単位で変更を追跡する仕組みなので、ファイルが1つもないディレクトリを「存在するもの」として扱えない。そこに空のファイルを1つ置くことで、ディレクトリの存在をGitに認識させる。
ファイル名は .gitkeep でも .keep でも .placeholder でも構わない。慣習として .gitkeep が広く使われている。
Gitが空ディレクトリを追跡できない理由(内部構造から見る)
これは「Gitの設計上の都合」というより、Gitのデータモデルそのものの帰結。理屈を一度押さえると、.gitkeep がなぜ必要なのか腑に落ちる。
Gitはリポジトリの状態を3種類のオブジェクトで表現する。
| オブジェクト | 中身 | 役割 |
|---|---|---|
blob | ファイルの中身(バイト列) | ファイル1つ分 |
tree | blob と他の tree への参照リスト | 1ディレクトリ分のスナップショット |
commit | 1つの tree への参照 + 親 commit + メタ情報 | 履歴の1ノード |
注目すべきは tree の中身。tree は「ファイル名 → blob/tree への参照」のリストでしかない。つまり、ファイルが1つもないディレクトリを表す tree を作っても、それは「中身ゼロのリスト」になり、参照先がないので親 tree には何も載せようがない。
これがGitが空ディレクトリを「保持できない」理由。SVNやMercurialのように「ディレクトリそのもの」を一級オブジェクトとして持つVCSとは設計が違う。
# 空ディレクトリを作っても git は何も検出しない
mkdir uploads
git status
# → uploads/ は何も表示されない("untracked files" にも出てこない)
.gitkeepの使い方
基本
空にしたいディレクトリに、中身ゼロのファイルを1つ置く。
# uploadsディレクトリを作成
mkdir uploads
# .gitkeepを作成(中身は空でOK)
touch uploads/.gitkeep
# これで git add できる
git add uploads/.gitkeep
git commit -m "Add uploads directory"
.gitignoreとの組み合わせ(実務で最も多いパターン)
「ディレクトリ構造は維持したいが、中身は除外したい」というケース。アップロード用フォルダ、キャッシュ、ログ出力先などで頻出する。
# uploads ディレクトリの中身は除外
uploads/*
# ただし .gitkeep は追跡する
!uploads/.gitkeep
この設定により、uploads/ 配下にどんなファイルを置いても無視されるが、ディレクトリ構造自体はリポジトリに含まれる。
実際の使用例(顧問先データ分離)
プロジェクトで顧問先ごとにデータを分離する構造を作った際の例。
tests/data/client_001/
├── inbox/ # スキャン待ちレシート
│ └── .gitkeep
├── batches/ # 処理済みバッチ
│ └── .gitkeep
├── exports/ # 出力済みファイル
│ └── .gitkeep
└── receipts.db # DBファイル(.gitignoreで除外)
.gitignore の設定はこうなる。
# データディレクトリ(中身は除外、構造は維持)
tests/data/client_*/inbox/*
tests/data/client_*/batches/*
tests/data/client_*/exports/*
!tests/data/client_*/**/.gitkeep
# DBファイルは除外
*.db
.gitkeep / .keep / .placeholder の違い
| 名前 | 使われる文脈 | 補足 |
|---|---|---|
.gitkeep | 個人プロジェクト、OSS全般で広く使用 | Git公式の機能ではなく慣習 |
.keep | Rails系のプロジェクトでよく見る | Railsジェネレータが自動生成する |
.placeholder | 「中身が後で来る予定」のニュアンス | 用途を伝える命名 |
重要な点: いずれも「Gitの公式機能」ではない。Gitから見れば、ただの「中身ゼロのファイル」。意図を読み手に伝える名前を選んでいるだけ。
.gitkeep を使うと「Gitの都合でここに置いているダミーである」ことが名前から伝わる。レビュー時に「これ何?」と聞かれにくい利点がある。
IDE/エディタでの扱い
- VSCode:
.gitkeepは通常のファイルとして表示される。エクスプローラーで非表示にしたい場合はfiles.exclude設定で**/.gitkeepを追加 - JetBrains系(IntelliJ / WebStorm 等): 同じく通常表示。Settings > Editor > File Types > Ignored Files で除外可能
- Vim/Neovim:
.で始まるファイルは隠しファイル扱い。:set listで確認可能 - macOS Finder / Windows Explorer: デフォルトでは隠しファイルとして非表示。
Cmd+Shift+./ 表示オプションで表示可能
実務でハマる落とし穴
落とし穴 1: .gitignore の * パターンが .gitkeep まで除外する
# NG: これだと .gitkeep も除外されてしまう
uploads/
# NG: 同じく除外される
uploads/**
# OK: 中身だけ除外して .gitkeep は残す
uploads/*
!uploads/.gitkeep
uploads/ という末尾スラッシュ付きや uploads/** は ディレクトリ全体 を除外する。.gitkeep も道連れになるため、uploads/* + !uploads/.gitkeep のセットが正解。
落とし穴 2: ネストしたディレクトリで .gitkeep が複数階層必要
data/
├── 2026/
│ ├── Q1/
│ │ └── .gitkeep # 必要
│ ├── Q2/
│ │ └── .gitkeep # 必要
│ └── .gitkeep # data/2026/ が空のことはほぼ無いが念のため
└── .gitkeep
data/ 配下の各四半期ディレクトリを維持したいなら、それぞれに .gitkeep を置く必要がある。1階層下の .gitkeep で上位ディレクトリが「保持される」と誤解しがち。
落とし穴 3: git rm で消すと .gitkeep ごと消える
# ディレクトリの中身を整理しようとして
git rm -r uploads/
# uploads/.gitkeep も消えてディレクトリが消滅
明示的に残す場合:
git rm -r uploads/
mkdir -p uploads/
touch uploads/.gitkeep
git add uploads/.gitkeep
.gitkeep を使わずに済むケース
実は、運用次第で .gitkeep を使わずに済むこともある。
- ビルド時に生成されるディレクトリ:
dist/.nuxt/node_modules/等は.gitignoreで完全に除外し、起動時にツールが自動作成するので不要 - README.md を入れる: 「このディレクトリは何のため?」を兼ねた説明書きを置けば、それが空ディレクトリ問題も解決する
.npmrc.envrcなどの設定ファイル: 機能ファイル自体が常に存在するなら、ダミーは不要
「.gitkeep を置く前に、本当に空でいいのか」を一度考える価値はある。
まとめ
- Gitは
treeオブジェクトの仕様上、空ディレクトリを保持できない .gitkeepは 慣習的なダミーファイル。中身ゼロのファイルを置けばいい.gitignoreと組み合わせるならuploads/*+!uploads/.gitkeepのパターン- ネストしたディレクトリは階層ごとに
.gitkeepが必要 - 実は使わずに済むケースもある(ビルド生成物・README 兼用)
関連記事
- Claude Code のセッション状態ファイルを .gitignore で整理する —
.gitignoreパターンの実例集 - Git worktree と wtp(worktree-plus)の運用 — 複数ブランチを並行作業する際のディレクトリ構成
- Git history を意味のあるコミットで残す — gitkeep のような細かいコミットをどうまとめるか
- GitHub Rulesets 設定ガイド — リポジトリ全体の運用ルール