Don't create .gitkeep files, use .gitignore instead (2023)

2026-02-2022:2720294adamj.eu

2023-09-18Git only tracks files, not directories. It will only create a directory if it contains a tracked file. But sometimes you need to “track” a directory, to ensure it exists for fresh clones of…

Crystal a or simpler crystal b?

Git only tracks files, not directories. It will only create a directory if it contains a tracked file. But sometimes you need to “track” a directory, to ensure it exists for fresh clones of a repository. For example, you might need an output directory called build.

In this post, we’ll look at two ways to achieve this. First, the common but slightly flawed .gitkeep technique, then a simpler one using only a .gitignore file.

This technique uses an empty file called .gitkeep:

The empty file ensures that Git creates the directory with minimal cost. Any other filename may be used, as Git doesn’t treat .gitkeep files any differently.

To set this up, you might create an empty file with touch:

Then ignore all files in the directory, except .gitkeep, by adding patterns in the repository’s .gitignore file:

/build/*
!/build/.gitkeep

The first pattern ignores everything in the build directory. The second one then un-ignores the .gitkeep file, allowing it to be committed.

This technique works, but it has some downsides:

  1. It requires editing two files.
  2. If the directory is renamed, .gitignore needs updating, which is easy to miss.
  3. .gitkeep is not a name recognized by Git, so there’s no documentation on it, potentially confusing other developers.

There’s a better way that doesn’t have these flaws.

This technique uses only a short .gitignore file inside the directory:

The .gitignore file has these contents:

The first pattern ignores all files in the directory. The second one then un-ignores the .gitignore file, so it can be committed.

You can create this file with echo and file redirection:

$ echo -e '*\n!.gitignore' > build/.gitignore

When you add and commit the directory, Git will pick up on the .gitignore file first, skipping other files within the directory:

$ git add build $ git status
On branch main
Changes to be committed:
 new file: build/.gitignore $ git commit -m "Track build directory"
[main 1cc9120] Track build directory
 1 file changed, 2 insertions(+)
 create mode 100644 build/.gitignore

The directory is now “tracked” with a single, standard file that will work even after renames.

Don’t ignore this technique,

—Adam

😸😸😸 Check out my new book on using GitHub effectively, Boost Your GitHub DX! 😸😸😸

One summary email a week, no spam, I pinky promise.

Related posts:

Tags: git

© 2023 All rights reserved. Code samples are public domain unless otherwise noted.


Read the original article

Comments

  • By jkubicek 2026-02-212:124 reply

    I'm not sure if I'm the one to blame for this or not, but the earliest reference to ".gitkeep" I can find online is my 2010 answer on Stack Overflow: https://stackoverflow.com/a/4250082/28422

    If this is all my fault, I'm sorry.

    • By pilaf 2026-02-218:36

      This Rails commit from May 2010 mentions gitkeeps and it's a few months older than your SO post, so it seems you're absolved from guilt:

      https://github.com/rails/rails/commit/785493ffed41abcca0686b...

    • By kazinator 2026-02-225:15

      Dummy empty files such as .keepme were used in CVS repositories for exactly the same purpose, and probably other version controls systems long before Git existed.

      The Peter Cederqvist manual recommended the practice.

      Here is a 1993 dated copy someone left hosted:

      https://www.astro.princeton.edu/~rhl/cvs/cvs.html

      The paragraph which recommends the .keepme files is:

      https://www.astro.princeton.edu/~rhl/cvs/cvs.html#SEC63

      "if you want an empty directory then put a dummy file (for example `.keepme') in it to prevent `-P' from removing it."

    • By hn92726819 2026-02-2114:514 reply

      Yeah... I don't think you were wrong. Having 100 tiny gitignores makes finding out why something is excluded annoying. Our policy is one root level gitgnore and gitkeeps where required.

      Some devs will just open the first gitignore they see and throw stuff into it. No thank you.

      • By zahlman 2026-02-2120:132 reply

        I like to make a .local folder at the top of the project, which contains a .gitignore that ignores everything. Then I can effortlessly stash my development notes there without affecting the project .gitignore or messing around within the .git directory.

        • By ddek 2026-02-2218:501 reply

          You can create a global gitignore in your home directory. I have ‘.<myname>’ ignored there, so if I ever create a directory with that name I know it’s contents won’t go into source control. That way I don’t have to edit the repositories gitignore with me-specific stuff.

          • By hobofan 2026-02-2222:31

            You wouldn't have to edit the actual repositories gitignore anyways. Every checkout of a repo comes with a .git/info/exclude file, which acts like a local additional gitignore file.

        • By beAbU 2026-02-2210:501 reply

          Why not put '.local' in your toplevel gitignore, and not commit an empty .local folder up to the forge?

          • By zahlman 2026-02-2218:35

            Upstream never sees an empty .local folder because, as established, Git doesn't keep empty folders. This way, .local isn't mentioned in the top-level .gitignore. It's just that tiny bit cleaner.

      • By predkambrij 2026-02-2120:05

        I share your view. .keep and .gitignore are different things. Having one .gitignore caputuring everything is less mental load.

      • By taftster 2026-02-2121:20

        I agree with you. Empty .gitignore would be a "smell" to me. Whereas .gitkeep tells me exactly what purpose it serves. I like the semantic difference here that you describe. I don't like when multiple .gitignore files are littered throughout the codebase.

      • By nickysielicki 2026-02-2120:261 reply

        > Having 100 tiny gitignores makes finding out why something is excluded annoying. Our policy is one root level gitgnore and gitkeeps where required.

        This is not a complicated or important enough problem to justify a team-wide policy. Let it work itself out naturally.

        https://git-scm.com/docs/git-check-ignore makes it trivial to debug repo-wide gitignore behavior.

        • By lucketone 2026-02-229:59

          We could say that practically all problems would work them self out one way or another.

          I heard facebook allows any language as long as you have packaged it neatly with all required build chain. (I.e. Even a choice of language works out)

          Some lowest common denominator will be reached. (1 :) )

          On the other hand: Are we happy with the lowest or do we want to aim higher?

    • By selridge 2026-02-212:281 reply

      This is delightful. Accidental load-bearing SO post.

      • By jkubicek 2026-02-212:321 reply

        It's especially funny since my answer is wrong anyway! The other top answer is much better. I did get a lot of early SO brownie points from that one answer though.

  • By Arrowmaster 2026-02-211:466 reply

    The author makes a very common mistake of not reading the very first line of the documentation for .gitignore.

      A gitignore file specifies intentionally untracked files that Git should ignore. Files already tracked by Git are not affected; see the NOTES below for details.
    
    You should never be putting "!.gitignore" in .gitignore. Just do `echo "*" > .gitignore; git add -f .gitignore`. Once a file is tracked any changes to it will be tracked without needing to use --force with git add.

    • By BlackFly 2026-02-216:46

      The point of that line is to robustly survive a rename of the directory which won't be automatically tracked without that line. You have to read between the lines to see this: they complain about this problem with .gitkeep files.

    • By ekipan 2026-02-212:081 reply

      Yeah, this. Plus a mistake from the article:

        $ echo '*\n!.gitignore' > build/.gitignore
      
      The \n won't be interpreted specially by echo unless it gets the -e option.

      Personally if I need a build directory I just have it mkdir itself in my Makefile and rm -rf it in `make clean`. With the article's scheme this would cause `git status` noise that a `/build/` line in a root .gitignore wouldn't. I'm not really sure there's a good tradeoff there.

      • By Aaron2222 2026-02-216:361 reply

        > The \n won't be interpreted specially by echo unless it gets the -e option.

        Author's probably using Zsh, which interprets them by default.

        • By _kst_ 2026-02-225:28

          If you want any kind of non-trivial formatting, use the printf command, not echo.

    • By AgentME 2026-02-212:12

      If you have a project template or a tool that otherwise sets up a project but leaves it in the user's hands to create a git repo for it or commit the project into an existing repo, then it would be better for it to create a self-excepting .gitignore file than to have to instruct the user on special git commands to use later.

    • By xg15 2026-02-2112:211 reply

      I think I'd prefer to have all ignores and un-ignores explicitly in the file and not have some of them defined implicitly because a file was added to tracking at some point.

      • By 1718627440 2026-02-2214:12

        But ignore files are only for untracked files anyway. Maybe you want them to specify what can be in the repo and what not, but this is not how Git works.

    • By nebezb 2026-02-216:09

      This is functionally the same. What do you mean by “you should never”? According to who?

      What an arrogant take. This is preference. Don’t mistake it for correctness.

    • By smrq 2026-02-214:18

      Why is this approach better than the author's?

  • By hollasch 2026-02-2120:221 reply

    My preference is to use the build system to create built artifacts, and I consider the build/ directory to be a built artifact. Wrangling Git into doing the first fundamental build step is off, in my opinion.

    However, if you disagree, my favorite "Git keep" filename is "README.md". Why is this otherwise empty directory here, how does it fit into my source tree, how is it populated, and so forth.

    One of my pet peeves with the latest AI wave is the time we spend creating files to help AI coding agents, but don't give the same consideration to the humans who have to maintain and update our code.

    • By taftster 2026-02-2121:26

      Both points here are appreciated. One that a README file as a "placeholder" for a directory gives the opportunity to describe why said empty directory exists. I would be slightly concerned though if my build process picked up this file during packaging. But that's probably a minor concern and your point stands.

      Additionally, the AI comment is ironic as well. It's like we're finally writing good documentation for the sake of agents, in a way that we should have been writing all along for other sentient consumers. It's funny to see documentation now as basically the horse instead of the cart.

HackerNews