How to Handle .gitignore in a Monorepo with Multiple Packages
Combine a root .gitignore for global artifacts with package-specific .gitignore files in each sub-directory, using templates from the GitHub gitignore repository to ensure comprehensive coverage.
Managing ignore patterns in a monorepo requires a hierarchical strategy that balances repository-wide rules with technology-specific exclusions. The github/gitignore repository provides battle-tested templates for every major language and tool, which you should adapt rather than writing rules from scratch. This approach prevents build artifacts, dependencies, and IDE files from polluting your git history while keeping each package's ignore logic modular and maintainable.
Create a Root .gitignore for Global Artifacts
Start with a root-level .gitignore that captures files common to all packages in your monorepo. These typically include operating system files, editor workspace configurations, and monorepo-tooling logs that appear across multiple projects.
According to the github/gitignore source code, you should pull patterns from the Global templates for cross-cutting concerns:
- OS-specific files – Use patterns from
Global/macOS.gitignoreorGlobal/Windows.gitignore(e.g.,.DS_Store,Thumbs.db) - IDE workspace files – Reference
Global/VisualStudioCode.gitignorefor*.code-workspaceandGlobal/JetBrains.gitignorefor.idea/**/workspace.xml - Monorepo tooling – The
Node.gitignoretemplate includeslerna-debug.log*for Lerna-based JavaScript monorepos
# Global OS artefacts
.DS_Store
Thumbs.db
# Editor / IDE workspace (do NOT commit)
*.code-workspace
.idea/**/workspace.xml
# Log files that appear in many tools
*.log
# Lerna (JS monorepo) temporary files
lerna-debug.log*
Keep this root file lean and generic. It should not contain language-specific build directories like node_modules/ or __pycache__/, as those belong in package-specific files.
Add Package-Specific .gitignore Files
Each package in your monorepo should maintain its own .gitignore file containing rules relevant only to that package's technology stack. This isolation ensures that a Python backend package doesn't inherit irrelevant Node.js patterns and vice versa.
Copy the appropriate template from github/gitignore directly into each package directory:
- Node.js packages – Copy from
Node.gitignoreto handlenode_modules/,npm-debug.log*, and build outputs - Python packages – Copy from
Python.gitignorefor__pycache__/,*.py[cod], and virtual environment artifacts - Rust packages – Copy from
Rust.gitignorefortarget/andCargo.lock(in libraries)
Example for a frontend/ package (React/Node.js):
# Node / npm artefacts (from Node.gitignore)
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build output
build/
dist/
# Environment files
.env
Example for a backend/ package (Django/Python):
# Python artefacts (from Python.gitignore)
__pycache__/
*.py[cod]
*.pyo
*.pyd
*.egg-info/
*.egg
*.whl
# Django specific
*.log
local_settings.py
db.sqlite3
/media
/staticfiles
Use Conditional Includes for Reusable Rules
When multiple packages share identical technology stacks, duplicating .gitignore content violates the DRY principle. Git provides two mechanisms to apply reusable rule files conditionally.
Option 1: Global Excludes File
Set a repository-wide excludes file that applies to all worktrees:
git config core.excludesFile .gitignore-global
Place common patterns (like those from Global/VisualStudioCode.gitignore) in this file to apply them everywhere without committing the configuration to each package.
Option 2: Conditional Includes (Git 2.13+)
Use the includeIf directive in your .gitconfig to load technology-specific rules only when working within certain directories:
[core]
excludesfile = .gitignore-global
[includeIf "gitdir:frontend/"]
path = .gitignore-node
[includeIf "gitdir:backend/"]
path = .gitignore-python
Store .gitignore-node and .gitignore-python as copies of the corresponding github/gitignore templates in your repository root. Git automatically applies the correct rules based on your current working directory.
Maintain and Verify Your Configuration
Keep Templates Current
The github/gitignore repository updates continuously as new tools emerge. Refresh your package templates periodically using raw GitHub URLs to avoid cloning the entire repository:
curl -L https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore > packages/frontend/.gitignore
curl -L https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore > packages/backend/.gitignore
Verify Pattern Application
After setup, confirm that your hierarchy works correctly using git check-ignore. This command identifies which .gitignore file is blocking a specific path:
git check-ignore -v packages/frontend/node_modules/some_pkg/index.js
git check-ignore -v packages/backend/__pycache__/module.cpython-38.pyc
The output displays the matching pattern and source file, allowing you to debug whether root or package-specific rules are catching the file.
Summary
- Root
.gitignorehandles OS files, IDE configurations, and monorepo-wide logs using patterns fromGlobal/templates ingithub/gitignore. - Package-specific files use dedicated templates like
Node.gitignoreorPython.gitignoreto handle build artifacts and dependencies local to each technology stack. - Conditional includes via
git configorincludeIfdirectives eliminate duplication when multiple packages share the same stack. - Verification with
git check-ignore -vensures your hierarchy applies rules exactly where intended.
Frequently Asked Questions
Can I use a single .gitignore file for the entire monorepo?
While technically possible using path-specific patterns (e.g., **/node_modules/), a single file becomes unmaintainable as your monorepo scales. Separate files allow packages to evolve independently—when you extract a package to its own repository, its .gitignore travels with it. The github/gitignore templates are also designed to be standalone, making per-package copies the path of least resistance.
How do I handle nested packages with different technologies?
Nest .gitignore files at every level where the technology changes. Git aggregates rules from all .gitignore files in the path to a given file, with the nearest file taking precedence. For a packages/mobile/ios/ directory containing a Swift project, place an iOS-specific .gitignore (from Swift.gitignore) inside that directory even if packages/mobile/ already contains a React Native .gitignore.
Should I commit the .gitignore templates or download them at build time?
Always commit the .gitignore files to your repository. These files stabilize your project structure and ensure every contributor ignores the same artifacts regardless of their local Git configuration. While you might use curl to initially populate or update the files, the committed versions serve as the source of truth for your team's development environment.
What happens when two .gitignore files conflict?
Git processes .gitignore files from the repository root down to the target file's directory, with later rules overriding earlier ones. A negation pattern (starting with !) in a package-level file can re-include a file ignored by the root. Use git check-ignore -v to see exactly which pattern is matching when troubleshooting conflicts between global and package-specific rules.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →