Setup gitignore
Contract
| Field | Bound contract |
|---|---|
| Trigger | set up gitignore / fix gitignore / untracked files keep appearing |
| Authority | Reversible-local: write only the repository .gitignore, with a snapshot before merging an existing file; never read or write a global excludes file. |
| Side effect | Writes or edits .gitignore; never ~/.gitignore, ~/.config/git/ignore, or git config --global. |
| Done | .gitignore has stable section anchors, composing it again with the same inputs produces identical bytes, and before/after untracked counts are reported. |
Inputs
No required user input. Operate on the current Git repository.
Optional input is a set of empirical ignore patterns explicitly confirmed by the user after reviewing current untracked-path clusters. Never infer confirmation and never add an empirical pattern silently.
Procedure
-
Locate the repository. Run
git rev-parse --show-toplevel, change the working directory used by this procedure to that exact root, and stop withnot-a-repositoryif it fails. Do not inspect global Git ignore configuration. Done when: the repository root is confirmed ornot-a-repositoryis reported. -
Detect template keys at depth two. Enumerate regular files at repository-relative depth 0, 1, or 2, excluding
.git/. Apply every matching row, deduplicate keys, and sort keys lexicographically before joining them with commas: Done when: the sorted key list is produced (possibly empty).| Evidence filename | gitignore.io key(s) | |---|---| |
tsconfig.json,tsconfig.*.json|node,typescript| |package.json|node| |pyproject.toml,requirements.txt,setup.py,setup.cfg,Pipfile|python| |Cargo.toml|rust| |go.mod|go| |dune-project,dune,*.opam|ocaml| |pom.xml,build.gradle,build.gradle.kts|java| |settings.gradle.ktswith Kotlin source files |java,kotlin| |CMakeLists.txt|cmake,c++| |Gemfile,*.gemspec|ruby| |composer.json|php|A filename may contribute several keys; an empty result is valid and means no network template is requested.
-
Capture current state. Count and retain the exact list from
git status --short --untracked-files=allentries beginning??, excluding.gitignore. If.gitignoreexists, read its exact bytes and copy it to/tmp/gitignore-snapshot-<UTC-YYYYMMDDTHHMMSSZ>.bakbefore modifying it. If snapshot creation fails, stop without writing. Done when: the untracked list is captured and the snapshot exists orsnapshot-failedis reported. -
Confirm empirical patterns. Group untracked paths first by top-level directory; group root files by lowercase extension, with extensionless root files as
(no extension). Show every path in each group. Ask the user which exact directory (name/) or extension (*.ext) patterns to add. Normalize confirmed patterns to repository-relative slash form, reject.., absolute paths, and patterns matching.gitignore, then sort and deduplicate them. Declining empirical additions means an empty empirical block, not cancellation of the language and local-baseline composition. Done when: empirical patterns are confirmed, normalized, and deduplicated. -
Fetch the language block. If the sorted key list is non-empty, run
scripts/compose-gitignore.sh <comma-separated-keys>. Use its stdout verbatim after normalizing line endings to LF and removing trailing blank lines. If the script fails, stop withtemplate-fetch-failedand leave.gitignoreunchanged; there is no silent fallback. If the key list is empty, the language block is empty and the script is not run. Done when: the language block is fetched and normalized ortemplate-fetch-failedis reported. -
Use these exact local baselines. They are inline data, not support-file placeholders:
# === AI TOOLING === .claude/settings.local.json .cursor/ .windsurf/ .aider.chat.history.md .aider.input.history # === IDE / EDITOR === .idea/ .vscode/* !.vscode/extensions.json !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json *.swp *.swo *~ .DS_Store Thumbs.dbDone when: the exact local baselines are available unchanged for section assembly.
-
Build managed sections. Produce these four anchors in this exact order, each followed by its normalized body and one blank line except the final block:
# === LANGUAGE TEMPLATES === <successful API body, or empty> # === AI TOOLING === <the five AI patterns above> # === IDE / EDITOR === <the ten IDE/editor patterns above> # === EMPIRICAL === <confirmed patterns, one per line>Treat a non-comment, non-blank pattern line as a duplicate when its exact trimmed text has already appeared earlier in unmanaged user content or an earlier managed section; first occurrence wins. Preserve comment lines supplied by the API. Do not introduce angle-bracket text into the actual file. Done when: the four managed sections are built in order with duplicates removed.
-
Merge deterministically. If no
.gitignoreexists, the candidate is the four managed sections. If one exists, require each managed anchor to occur zero or one time. Duplicate or out-of-order managed anchors areinvalid-managed-sections. Preserve all bytes before the first managed anchor and after/between managed regions that are not part of a managed block. Replace each existing managed block from its anchor through the line before the next managed anchor; append missing managed blocks in canonical order. Normalize only generated managed blocks to LF; do not rewrite preserved user content. Ensure exactly one terminal newline. Done when: the candidate is merged with preserved user content and exactly one terminal newline, orinvalid-managed-sectionsis reported. -
Obtain write approval. Show the complete before/candidate diff using any available local diff renderer; do not require
difft. If the user declines, leave.gitignoreunchanged. If approved, write the candidate to.gitignoreatomically in the repository root. Done when: the user approves and the file is written, or the user declines and the file is unchanged. -
Prove idempotence and report. Run steps 2, 5, 7, and 8 in memory against the newly written bytes with the same confirmed empirical inputs. Require the second candidate to be byte-identical; otherwise restore the snapshot (or remove a newly created
.gitignore) and reportidempotence-failed. Recount untracked entries with the same command as step 3 and report both counts plus every remaining untracked path. Done when: the second candidate is byte-identical and before/after untracked counts are reported, oridempotence-failedis reported.
Failure and recovery
| Failure class | Recovery |
|---|---|
| not-a-repository | Stop; no file is written. |
| snapshot-failed | Stop before modifying .gitignore. |
| template-fetch-failed | Report keys and network error; leave .gitignore unchanged. Retry only on an explicit new run. |
| invalid-empirical-pattern | Show the rejected pattern and ask for a repository-relative replacement; do not write meanwhile. |
| invalid-managed-sections | Report duplicate or out-of-order anchors; leave the original file unchanged for manual repair. |
| write-declined | Leave the original file unchanged. |
| write-failed | Restore the snapshot, or remove a partially created new file. |
| idempotence-failed | Restore the snapshot, or remove the new file; report the second-pass diff. |
Output
The repository-root .gitignore, created or updated with LANGUAGE TEMPLATES, AI TOOLING, IDE / EDITOR, and EMPIRICAL anchors. The response reports before/after untracked counts, remaining paths, and the snapshot path when an existing file was merged.