---
title: The Case for a .gitignore Allowlist
description: A gitignore allowlist ignores everything by default and re-includes only what you track. It flips the denylist habit, with one directory-traversal gotcha.
canonical: https://lukeocodes.dev/gitignore-allowlist
type: article
locale: en_GB
published: 2026-09-05T00:00:00.000Z
modified: 2026-09-05T00:00:00.000Z
authors:
  - Luke Oliff
section: Git
tags:
  - Git
  - Opinion
  - gitignore
  - denylist
  - monorepo
  - config
feed: https://lukeocodes.dev/rss.xml
search: https://lukeocodes.dev/search.json?q={query}
schema:
  - "@context": https://schema.org
    "@graph":
      - "@type": WebSite
        "@id": https://lukeocodes.dev/#website
        url: https://lukeocodes.dev
        name: Luke Oliff
        alternateName: lukeocodes
        inLanguage: en
        publisher:
          "@id": https://lukeocodes.dev/authors/luke#person
        description: Personal blog and writing by Luke Oliff — developer relations, voice AI, developer experience.
        potentialAction:
          - "@type": SearchAction
            target:
              "@type": EntryPoint
              urlTemplate: https://lukeocodes.dev/blog/search?q={search_term_string}
            query-input: required name=search_term_string
      - "@type": Person
        "@id": https://lukeocodes.dev/authors/luke#person
        name: Luke Oliff
        url: https://lukeocodes.dev/authors/luke
        description: Engineer and developer relations leader. Writing about developer relations, AI, and the voice industry, from the UK.
        mainEntityOfPage: https://lukeocodes.dev/authors/luke
        givenName: Luke
        familyName: Oliff
        image: https://lukeocodes.dev/authors/luke.jpg
        jobTitle: Engineer and developer relations leader
        sameAs:
          - https://www.linkedin.com/in/lukeocodes
          - https://github.com/lukeocodes
          - https://bsky.app/profile/lukeocodes.dev
          - https://x.com/lukeocodes
          - https://hashnode.com/@lukeocodes
          - https://dev.to/lukeocodes
        knowsAbout:
          - Developer Relations
          - Developer Experience
          - Voice AI
          - Text-to-Speech
          - Answer Engine Optimisation
          - Search Engine Optimisation
          - Open Source
          - Developer Tooling
          - API Design
          - Speech-to-Text
          - Voice Agents
          - Community Building
        knowsLanguage:
          - English
        nationality: British
        alumniOf:
          - "@type": Organization
            name: The Open University
  - "@context": https://schema.org
    "@graph":
      - "@type": Person
        "@id": https://lukeocodes.dev/authors/luke#person
        name: Luke Oliff
        url: https://lukeocodes.dev/authors/luke
        description: Engineer and developer relations leader. Writing about developer relations, AI, and the voice industry, from the UK.
        mainEntityOfPage: https://lukeocodes.dev/authors/luke
        givenName: Luke
        familyName: Oliff
        image: https://lukeocodes.dev/authors/luke.jpg
        jobTitle: Engineer and developer relations leader
        sameAs:
          - https://www.linkedin.com/in/lukeocodes
          - https://github.com/lukeocodes
          - https://bsky.app/profile/lukeocodes.dev
          - https://x.com/lukeocodes
          - https://hashnode.com/@lukeocodes
          - https://dev.to/lukeocodes
        knowsAbout:
          - Developer Relations
          - Developer Experience
          - Voice AI
          - Text-to-Speech
          - Answer Engine Optimisation
          - Search Engine Optimisation
          - Open Source
          - Developer Tooling
          - API Design
          - Speech-to-Text
          - Voice Agents
          - Community Building
        knowsLanguage:
          - English
        nationality: British
        alumniOf:
          - "@type": Organization
            name: The Open University
      - "@type": BlogPosting
        "@id": https://lukeocodes.dev/gitignore-allowlist#article
        headline: The Case for a .gitignore Allowlist
        alternativeHeadline: The Case for a .gitignore Allowlist
        description: A gitignore allowlist ignores everything by default and re-includes only what you track. It flips the denylist habit, with one directory-traversal gotcha.
        datePublished: 2026-09-05T00:00:00.000Z
        dateModified: 2026-09-05T00:00:00.000Z
        url: https://lukeocodes.dev/gitignore-allowlist
        mainEntityOfPage: https://lukeocodes.dev/gitignore-allowlist
        inLanguage: en
        author:
          - "@id": https://lukeocodes.dev/authors/luke#person
        isPartOf:
          "@id": https://lukeocodes.dev/#website
        keywords: Git, Opinion, gitignore, denylist, monorepo, config
        articleSection: Git
        about:
          - "@type": SoftwareApplication
            name: Git
        abstract: "Flip it round: ignore /**/* then re-include with !**/ for directories and !path for files. A gitignore allowlist stops junk landing by default, at the cost of a starter template and one traversal gotcha."
        speakable:
          "@type": SpeakableSpecification
          cssSelector:
            - .post-tldr
            - h1
        image: https://lukeocodes.dev/og.png?h=The+Case+for+a+.gitignore+Allowlist&d=A+gitignore+allowlist+ignores+everything+by+default+and+re-includes+only+what+you+track.+It+flips+the+denylist+habit%2C+with+one+directory-traversal+gotcha.&e=Writing
        wordCount: 566
        timeRequired: PT3M
        publisher:
          "@id": https://lukeocodes.dev/authors/luke#person
      - "@type": BreadcrumbList
        itemListElement:
          - "@type": ListItem
            position: 1
            name: Home
            item: https://lukeocodes.dev
          - "@type": ListItem
            position: 2
            name: Writing
            item: https://lukeocodes.dev/blog
          - "@type": ListItem
            position: 3
            name: The Case for a .gitignore Allowlist
            item: https://lukeocodes.dev/gitignore-allowlist
      - "@type": FAQPage
        "@id": https://lukeocodes.dev/gitignore-allowlist#faq
        mainEntity:
          - "@type": Question
            name: Why doesn't unignoring a file inside an ignored folder work?
            acceptedAnswer:
              "@type": Answer
              text: Git skips ignored directories entirely for performance, so it never looks inside to see your negation. You have to re-include the directory first with a pattern like !**/, then re-include the file. Order matters here.
          - "@type": Question
            name: What does the !**/ line do in a gitignore allowlist?
            acceptedAnswer:
              "@type": Answer
              text: It re-includes every directory after /**/* ignored everything. That lets git descend into folders to find the specific files you allow next. Without it, git never traverses in and your !file lines match nothing.
          - "@type": Question
            name: Isn't an allowlist more maintenance than a denylist?
            acceptedAnswer:
              "@type": Answer
              text: Up front, yes. You add a line each time you introduce a new file type you want tracked. In return nothing gets committed by accident, which is the failure mode a denylist can't prevent. It's a real tradeoff, not a free win.
          - "@type": Question
            name: Does this replace a global gitignore?
            acceptedAnswer:
              "@type": Answer
              text: No, it complements it. Keep editor and OS junk like .DS_Store in your global core.excludesfile. Use the per-repo allowlist to control what actually belongs in that specific project.
---

[Luke Oliff.](/)

- [Blog](/blog)
- [CV](/cv)
- [Uses](/uses)
- [User manual](/user-manual)
- [Living with ADHD](/living-and-working-with-adhd)
- [RSS](/rss.xml)

[Luke Oliff · Writing](/blog)

# The Case for a .gitignore Allowlist

Sep 2026 · [Git](/blog/category/git) · 3 min read · [Luke Oliff](/authors/luke)

**TL;DR:** Flip it round: ignore /**/* then re-include with !**/ for directories and !path for files. A gitignore allowlist stops junk landing by default, at the cost of a starter template and one traversal gotcha.

A gitignore allowlist inverts the file: instead of listing what to ignore, you ignore everything and then re-include only what you want tracked. It's `/**/*` to ignore the lot, `!**/` to let git see into directories again, then a `!` line per thing you actually commit. The payoff is that nothing lands by accident. The cost is a starter template and one gotcha about directories.

This did the rounds on Hacker News in September 2026 after [Alex Pliutau argued for it on packagemain.tech](https://packagemain.tech/p/gitignore-everything-by-default). It's a contrarian take, and I think it's right more often than the internet does.

## The problem with the denylist

The default `.gitignore` is a denylist. Everything is tracked unless you name it. That works right up until the moment it doesn't, and the failure is always the same: a `.env` with real credentials, a 200MB build artifact, an editor's `.idea/` folder, all committed because nobody had added them to the ignore list yet. The denylist only protects you from the junk you already thought of. New junk sails straight through.

A committed secret is the expensive version of this. Once it's in history it's in history, and rotating the credential is the only real fix. The denylist had one job and it failed silently.

## What an allowlist looks like

Flip the default. The pattern, drawn from the [gitignore docs](https://git-scm.com/docs/gitignore) and the write-ups doing the rounds, is short:

```gitignore
# ignore everything
/**/*

# but let git descend into directories
!**/

# then allow only what you track
!/.gitignore
!/README.md
!/src/
!**/*.go
```

Read it top to bottom. Line one ignores every file at every depth. Line two re-includes directories so git will actually look inside them. The rest name what you keep: the ignore file itself, your readme, the source tree, all `.go` files anywhere. Add a language or a config type, add a line. Everything else is invisible to git by default.

## The gotcha that makes people give up

Here's where the first attempt usually fails. You write `/**/*` then `!src/app.go` and it does nothing. The file stays ignored.

The reason is a performance shortcut in git: it does not descend into an ignored directory at all. If `src/` is ignored, git never opens it, so it never sees your negation for a file inside it. You cannot re-include a file whose parent directory is excluded. That `!**/` line is not decoration, it's the thing that makes the whole approach work, because it un-ignores directories so git will traverse them and reach the files you allowed. Miss it and the allowlist looks broken.

## Where I land

The denylist is the right default for a scratch repo or a quick prototype, where you'll commit anything and clean up later. For anything with secrets, build output, or more than one contributor, the allowlist is the safer shape and I'd take the extra maintenance every time. You pay a line of config per new file type. You stop paying for leaked credentials and 200MB commits you have to rewrite history to remove.

It's more work, and the work is the point. An allowlist forces a decision, "does this belong in the repo", at the moment you add a new kind of file, which is exactly when you have the context to answer it. A denylist defers that decision until something has already gone wrong. Pick the one that fails safe.

[← Newer: Running a Frontier-Class Local LLM on One GPU](/local-llm-gpu)

[All posts](/blog) →

## Search this site

`GET https://lukeocodes.dev/search.json?q=<words>` returns matching posts as JSON, each with its page URL and its markdown URL. Add `&limit=<1-50>` for more than 10. How it works: [/blog/search](https://lukeocodes.dev/blog/search.md).

```sh
curl "https://lukeocodes.dev/search.json?q=developer+relations"
```

---

© 2026 Luke Oliff. Posts live at the root of this site.

- [GitHub](https://github.com/lukeocodes)
- [LinkedIn](https://www.linkedin.com/in/lukeocodes)
- [X](https://x.com/lukeocodes)
- [Bluesky](https://bsky.app/profile/lukeocodes.dev)
- [Email](mailto:luke@lukeoliff.com)
- [RSS](/rss.xml)

## Cookies

This site sets one analytics cookie, from Google Analytics, and only in a browser that accepts the cookie banner. A client that does not run scripts sets no cookies, so there is nothing to accept or decline here.

[Cookie policy](/cookies)
