Skip to content

Tracking Empty Directories in Git with .gitkeep

Git tracks files, not directories. That distinction is easy to overlook until you run into the consequence: an empty directory you create locally simply does not exist after a fresh clone. No staging, no commit, no push will capture it, because Git has nothing to work with. The .gitkeep convention is the widely adopted workaround for this behavior, and it requires nothing more than a single empty file placed inside the directory you need to preserve.

Why Git Ignores Empty Directories

Git's internal storage is built around objects: blobs for file contents, trees for directory listings, commits for snapshots. A directory entry in a tree object is only written when it contains at least one tracked file. An empty directory contributes no blobs and no tree entries, so Git has no object to store and nothing to track.

This is not an oversight. The design is intentional and consistent. But it creates a friction point whenever a project depends on a specific directory structure being present at runtime or during a build:

  • A web application that writes request logs to logs/ expects the directory to exist before the first request is handled.
  • A build pipeline that writes compiled output to dist/ will fail if the directory is absent when the script runs.
  • A project scaffold that communicates an intended folder structure to new contributors does not survive a fresh clone if those folders are empty.

The directory exists on the original developer's machine because it was created there. For anyone who clones the repository, it does not.

The Solution: A Placeholder File

The fix is to place a file inside the directory so that Git has something to track. The community settled on .gitkeep as the conventional name for this placeholder. The name communicates intent at a glance: this file is here to keep the directory in version control, nothing more.

Creating a .gitkeep file is a single command on macOS or Linux:

touch logs/.gitkeep

Then stage and commit it like any other file:

git add logs/.gitkeep
git commit -S -m "chore: add .gitkeep to preserve logs directory"

From that point on, logs/ will be present in every clone of the repository.

What .gitkeep Actually Is

Not an Official Git Feature

Unlike .gitignore or .gitattributes, the name .gitkeep has no special meaning to Git. Git does not recognize it, parse it, or behave differently because of it. From Git's perspective, .gitkeep is an ordinary file like any other. The convention works because the file is present, not because of what it is named. Some teams use .keep or a brief README.md for the same purpose.

Hidden by Default on Unix-Based Systems

The leading dot makes .gitkeep a hidden file on macOS and Linux. It will not appear in a standard ls listing or in most file managers unless hidden files are explicitly shown. The directory stays visually clean during day-to-day development.

Typically Empty

Most .gitkeep files are completely empty. Some teams add a one-line comment explaining why the directory needs to be preserved. Both approaches work; the important thing is consistency within a project.

Common Use Cases

Build and Output Directories

Build tools often require output directories to exist before they run. Rather than adding directory creation steps to each build script or CI workflow, a .gitkeep ensures the directories are always in place after a clone:

project/
├── src/
├── dist/
│   └── .gitkeep
└── build/
    └── .gitkeep

Log and Temporary File Directories

Applications that write logs or temporary files to a specific directory require that directory to exist at startup. A .gitkeep handles this at the repository level, which is a cleaner separation than adding directory creation logic inside the application:

project/
├── app/
└── logs/
    └── .gitkeep

Project Scaffolding

When establishing the folder structure for a new project, you often want contributors to see the intended layout before any real content exists in those directories. A .gitkeep in each empty directory makes the structure visible immediately on clone:

project/
├── assets/
│   ├── images/
│   │   └── .gitkeep
│   ├── fonts/
│   │   └── .gitkeep
│   └── icons/
│       └── .gitkeep
└── docs/
    └── .gitkeep

Combining .gitkeep with .gitignore

A common pattern is to use .gitkeep and .gitignore together: track the directory itself while ensuring its contents are never committed. The setup for a logs/ directory looks like this.

First, add the ignore rules to .gitignore before creating the placeholder:

# Ignore log file contents but preserve the directory
logs/*
!logs/.gitkeep

Then create the placeholder and commit:

touch logs/.gitkeep
git add logs/.gitkeep .gitignore
git commit -S -m "chore: track logs directory, ignore its contents"

The logs/* rule tells Git to ignore everything inside logs/. The !logs/.gitkeep exception carves out the placeholder so the directory itself is preserved. Anyone who clones the repository gets a logs/ directory ready to use, and nothing written into it will show up in git status or a commit.

Tip

The same pattern applies to any directory that should exist at runtime but whose contents should stay local: tmp/, cache/, uploads/, and similar directories are all good candidates.

.gitkeep vs. .keep

Both names are in common use. .gitkeep is more descriptive and more widely recognized, making it the better default. .keep is shorter and works just as well, but someone unfamiliar with the convention may not immediately understand why it is there. Whichever name you choose, apply it consistently across the project.

The convention is a small thing, but it is one of those details that saves real friction the first time a teammate clones a repository and wonders why the logs/ directory does not exist. A single empty file, a clear name, and the problem does not come up again.