Keeps your .gitignore maintained, not generated — curated blocks in a region the tool owns, everything else untouched
npx @kirchdev/gitignore-sync init # fingerprint the repo, confirm, write the region
npx @kirchdev/gitignore-sync sync # re-render it, any time, as often as you likeThat's it. The blocks the tool owns stay current; every line you wrote yourself survives verbatim.
gitignore.io, gig and ignr all fetch and dump: you pull a template once, paste it, and drift from there. None of them re-syncs, so a .gitignore grows into a few hundred lines of ballast nobody dares touch.
gitignore-sync keeps a managed region inside the file and re-renders it on demand. The rest of the file is a free zone the tool never writes to — which is what makes running it a second time safe, and a hundredth time boring.
pnpm add -g @kirchdev/gitignore-sync # npm i -g / yarn global add / bun add -g all workOr run it without installing: npx @kirchdev/gitignore-sync <command>. The binary is also available as gis.
gitignore-sync init # detect, confirm in a prompt, write the region
gitignore-sync edit # tick stacks on and off later
gitignore-sync add nuxt tauri # or name them — for scripts and CI
gitignore-sync remove intellij # drop one; your own lines are kept
gitignore-sync sync # re-render from the header
gitignore-sync check # CI gate: non-zero on drift
gitignore-sync list # what this binary ships, and what you declare
gitignore-sync info # which build is running, and what it sees
gitignore-sync audit ../*/ # how much of a whole estate the stacks cover
gitignore-sync check -r # every managed region in the tree, not just the rootEvery command takes --help and a directory (--dir for add/remove, positional elsewhere), and every writing command takes --dry-run.
Tip
init shows everything it found — your editor and platform included — and asks once:
ℹ Found 8 stacks: core, git, node, nuxt, tauri, dotenv, vscode, linux
? Use them? › Yes / No
In CI it asks nothing — --yes or no terminal takes the committed fingerprints alone, says what it skipped, and writes. Your platform must not end up in a committed header because a pipeline ran.
- 🔁 Re-syncs, never re-dumps —
syncis idempotent, so it belongs in a habit, a hook or a cron, not in a one-off ritual. - 🛡️ Your lines are never lost — a hand-written line found inside a managed block is moved to the free zone, not deleted. That rule is what makes the second run safe.
- 🔍 Dedup that knows git — exact duplicates go;
.idea,.idea/,/.ideaand.idea/*are four different patterns to git, so they are reported, never silently merged. - 🧹 Orphaned headings swept — when a managed block absorbs every pattern under a
# Comment, the heading goes with them. A block that was only ever a note stays. ⚠️ Catches the mistake that breaks!— a stray.vscodebeside a!.vscode/extensions.jsonblock silently disables it, because git never looks inside an ignored directory. That gets its own warning.- 🧭 No surprise rewrites —
initdetects,syncdoes not. Adding apackage.jsonnever quietly rewrites your.gitignore;sync --detectproposes and stops there. - 🗂️ Folds in your editor — the region uses
# region/# endregion, so VSCode collapses a 40-line managed block to a single line, at both nesting levels. - 📋 A real CI gate —
checkreports drift and duplicates and exits non-zero, so a stale block fails the build instead of rotting.
# region gitignore-sync
# stacks: core, node, vscode, intellij
# ─────────────────────────────────────────
# region core@v1
.DS_Store
.claude/settings.local.json
# endregion
# region node@v1
node_modules
dist
coverage
# endregion
# endregion
# ─── your rules, never touched ───
frankenphp
/bootstrap/ssrEverything below the closing # endregion is the free zone: yours, preserved line for line.
Twenty-one curated blocks, each derived from what the estate actually ignores rather than from a public template site.
| Stack | What it covers | Proposed by init when |
|---|---|---|
core |
.DS_Store |
always |
agents |
agent working files, not their config | .claude/, .codex/, … |
git |
merge and backup droppings | always (it is a git repo) |
node |
modules, build output, logs, caches | package.json |
dotenv |
.env*, minus the committed example |
.env.example or .env |
php |
/vendor, PHPUnit caches |
composer.json |
laravel |
build output, storage keys, SSR | artisan |
go |
test and build droppings | go.mod |
tofu |
state, tfvars, local overrides | *.tf / *.tofu |
nuxt |
.nuxt, .output, .nitro |
nuxt.config.* |
tauri |
src-tauri/target, generated schemas |
src-tauri/ |
rust |
/target |
a root Cargo.toml |
turborepo |
.turbo |
turbo.json |
playwright |
test-results, reports | playwright.config.* |
storybook |
storybook-static |
.storybook/ |
vscode |
.vscode/* + the shared files |
you have a .vscode/ |
intellij |
.idea/* |
you have a .idea/ |
vim |
swap and session files | your $EDITOR is vim |
macos |
AppleDouble, Spotlight, Trashes | you are on a Mac |
windows |
Thumbs.db, desktop.ini, Recycle Bin | you are on Windows |
linux |
*~, trash and NFS droppings |
you are on Linux |
gitignore-sync list prints this for the binary you have installed, marking the ones your repo declares.
There is no config file. The configuration is the # stacks: line in the .gitignore itself — the header is input, the blocks below it are output.
| Edit | Effect after sync |
|---|---|
Add a name to # stacks: |
Its block is rendered into the region |
Remove a name from # stacks: |
Its block disappears; your own lines are kept |
| Nothing | Nothing — sync is a no-op on a synced file |
Important
Nothing in a repository can tell you what the other contributors use — .vscode/ and .idea/ are themselves ignored. So those stacks are proposed from your own machine and written down once, rather than re-guessed on every run. Only repository fingerprints reach the non-interactive path; a CI runner's platform must not end up in a committed header.
- run: npx @kirchdev/gitignore-sync checkcheck writes nothing. It exits non-zero when the file has drifted from its header, and prints what it would have changed.
There is also an action, for a job summary on failure and a status output:
- uses: kirchDev/gitignore-sync@v0.1.0
with:
version: '0.1.0' # pin it, so a release cannot turn a green pipeline redNote
--recursive on audit and check walks the whole tree, skipping build output, dependencies, nested repositories, and anything the repo's own .gitignore names. Laravel's storage/ stubs and other directory keepers (* plus !.gitignore) are listed as skipped rather than measured — the framework owns them.
Tip
Repos whose CI derives its task list from package.json need neither — add
gitignore-sync check to your check script and it comes along.
PRs welcome. Conventional Commits required (enforced via commitlint). Husky runs the project's linters/formatters on git commit.
Tip
Run pnpm check:fix before pushing — CI will catch what husky missed.
See CONTRIBUTING.md for the full workflow.
Semantic Versioning via release-please — see CHANGELOG.md.