The Silent Un-Ignored File

Moving a directory in a Git repository can have an unintended consequence: files that were previously ignored can become visible to git status. This isn't a bug in Git, but a consequence of how the .gitignore specification handles patterns relative to the location of the .gitignore file itself. A developer recently discovered this firsthand when a file containing live credentials was silently un-ignored after a directory restructure. The file had been hidden for months, and a careless git add -A could have pushed sensitive data to a remote repository.

The incident highlights a less-discussed aspect of the .gitignore specification. While many developers are familiar with basic patterns, the nuances of how patterns are interpreted based on their file's location can lead to unexpected behavior, especially during refactoring operations. In this case, the repository contained a Swift app, and the addition of an Android app led to a lopsided directory structure. To address this, the Swift components were moved under a new apple/ directory, placing it alongside an existing android/ directory.

How .gitignore Patterns Work

Git's ignore rules are defined in .gitignore files. These files can exist at the root of the repository or in subdirectories. The interpretation of a pattern in a .gitignore file depends on the directory in which the file resides.

Patterns in a .gitignore file are relative to the directory containing the file. For example, if you have a .gitignore file at the root of your repository (.git/info/), a pattern like build/ will ignore any directory named build anywhere in the repository. However, if a .gitignore file is placed inside a subdirectory, say src/.gitignore, then a pattern like build/ within that file will only ignore a build directory located directly within the src directory (i.e., src/build/).

The critical rule here, often overlooked, states that a pattern such as /Documentation/ will only match a directory named Documentation at the root of the repository. If you move this Documentation directory to another location, like src/Documentation/, the pattern will no longer match because it's no longer at the root. Conversely, a pattern without a leading slash, such as Documentation/, will match a directory named Documentation at any level of the directory tree.

The problematic pattern in the incident was likely one that relied on the directory's original location. When the directory containing the sensitive file was moved, the pattern that previously matched it no longer did, effectively un-ignoring the file.

The Refactoring Scenario

The repository in question managed a Swift application. As an Android application was introduced, the top-level structure became unbalanced. Initially, the Swift code resided in various top-level directories and scripts, while the Android app was also spread out. To achieve a cleaner, more organized structure, the Swift components were consolidated under a new apple/ directory. This placed apple/ and android/ as sibling directories at the repository's root.

The move was performed using Git's git mv command. This command is designed to move or rename files and directories while preserving their history within Git. Crucially, the contents of the files themselves did not change, nor did the build processes for either the Swift or Android applications. All automated checks continued to pass, and both platforms remained buildable.

The issue arose because the .gitignore rules were not updated to reflect the new directory structure. A pattern that previously correctly ignored a sensitive file within the Swift project's original location now failed to match after the move. Git, following its rules, no longer considered the file ignored and presented it as a candidate for staging.

Diagram illustrating how .gitignore patterns are relative to the .gitignore file's location.

The Danger of Silent Failures

The most alarming aspect of this incident is the silent nature of the failure. Git did not issue any warnings or errors. git status simply began listing the previously ignored file as untracked and ready for staging. This lack of explicit feedback is where the danger lies. Developers, accustomed to .gitignore reliably hiding sensitive or build-generated files, might not scrutinize the output of git status closely, especially after a refactoring operation where they expect only structural changes.

Running git add -A, which stages all changes in the repository (including new, modified, and deleted files), without carefully reviewing the output of git status could lead to the accidental commit of the sensitive file. In this specific case, the file contained a Supabase URL and key, critical credentials that, if exposed, could compromise the database and associated services.

This scenario underscores the importance of not only understanding the syntax of .gitignore but also how its rules behave dynamically with directory restructuring. When refactoring involves moving directories that contain or are near ignored files, a thorough review of the .gitignore file and its interaction with the new structure is essential. It's also a reminder that automated security checks should ideally include scans for accidentally staged secrets, even if they were previously ignored.

Mitigation and Best Practices

To prevent similar incidents, developers should adopt several best practices:

  • Review .gitignore after Refactoring: Whenever significant directory moves or renames occur, explicitly review your .gitignore file. Ensure that existing patterns still correctly match the intended files and directories in their new locations.
  • Use Anchored Patterns: For critical files or directories that must *always* be ignored regardless of their location, use patterns anchored to the repository root. For example, /config/secrets.yml will ignore secrets.yml only if it's in the root directory. If you want to ignore secrets.yml anywhere, use secrets.yml. If you want to ignore a specific directory at the root, use /my_secret_dir/.
  • Test Your .gitignore: After making changes to .gitignore or refactoring, use git ls-files --others --exclude-standard to see which untracked files Git is aware of. This command lists files that are not tracked and are not ignored by your current .gitignore rules.
  • Automated Secret Scanning: Integrate tools like git-secrets, gitleaks, or TruffleHog into your pre-commit hooks or CI/CD pipeline. These tools scan code for exposed secrets, providing an additional layer of defense against accidental commits.
  • Principle of Least Privilege for Secrets: Ensure that any secrets committed (even accidentally) have the minimum necessary privileges. This limits the blast radius if they are exposed.

The incident serves as a valuable lesson: .gitignore is not a static shield. Its effectiveness is tied to the repository's structure, and structural changes require a re-evaluation of ignore rules. Developers must be vigilant, especially when refactoring, to avoid inadvertently exposing sensitive information.