{"id":3956,"date":"2026-10-11T20:45:44","date_gmt":"2026-10-11T18:45:44","guid":{"rendered":"https:\/\/planetozh.com\/blog\/?p=3956"},"modified":"2026-10-11T20:45:44","modified_gmt":"2026-10-11T18:45:44","slug":"a-per-repo-untracked-gitignore-local","status":"publish","type":"post","link":"https:\/\/planetozh.com\/blog\/2026\/10\/a-per-repo-untracked-gitignore-local\/","title":{"rendered":"A per-repo, untracked `.gitignore-local`"},"content":{"rendered":"<p>You have files that you want to ignore, but that have no business being in the project&#39;s <code>.gitignore<\/code>: a a local test script, some draft assets. Adding them to <code>.gitignore<\/code> would mean committing your personal clutter for everyone. Leaving them alone means <code>git status<\/code> keeps nagging about untracked files.<\/p>\n<p>Git has a built-in answer for this, <code>.git\/info\/exclude<\/code>, but it&#39;s tucked away inside <code>.git\/<\/code> where I never look, and I tend to forget what&#39;s in it. I wanted something visible, right at the root of the repo, that works the same way in all my local repositories.<\/p>\n<h2>Step 1: a global excludes file<\/h2>\n<p>Git lets you define an additional ignore file with <code>core.excludesfile<\/code>. Most people point it to a global file in their home directory. The trick here is to give it a <strong>relative<\/strong> path instead:<\/p>\n<pre class=\"match-braces line-numbers\"><code class=\"language-bash\">git config --global core.excludesfile .gitignore-local\n<\/code><\/pre>\n<p>Every repo now can have its own optional <code>.gitignore-local<\/code>, read just like a regular <code>.gitignore<\/code>, but never shared.<\/p>\n<p>There&#39;s one catch: the <code>.gitignore-local<\/code> file itself shows up as untracked. The fix is to make it ignore itself, by having <code>.gitignore-local<\/code> as its first line:<\/p>\n<pre class=\"match-braces line-numbers\"><code>.gitignore-local\n# local files you want to ignore but not share via regular .gitignore:\ntest-script.py\nassets\/img\/test-logo.png\n<\/code><\/pre>\n<p>Doing that by hand in every repo gets old fast.<\/p>\n<h2>Step 2: a <code>git ignore-local<\/code> alias<\/h2>\n<p>So here&#39;s an alias that takes care of it:<\/p>\n<pre class=\"match-braces line-numbers\"><code class=\"language-bash\">git config --global alias.ignore-local '!f() {\n    file=.gitignore-local\n    grep -qxF &quot;$file&quot; &quot;$file&quot; 2&gt;\/dev\/null || echo &quot;$file&quot; &gt;&gt; &quot;$file&quot;\n    [ $# -eq 0 ] &amp;&amp; { cat &quot;$file&quot;; return; }\n    for p in &quot;$@&quot;; do\n        grep -qxF -- &quot;$p&quot; &quot;$file&quot; || echo &quot;$p&quot; &gt;&gt; &quot;$file&quot;\n    done\n}; f'\n<\/code><\/pre>\n<p>What it does:<\/p>\n<ul>\n<li>Shell aliases (the ones starting with <code>!<\/code>) always run from the root of the repository, so the file lands in the right place even if you call the alias from a subdirectory.<\/li>\n<li>If <code>.gitignore-local<\/code> doesn&#39;t exist yet, or exists without its self-ignoring line, that line is added.<\/li>\n<li>Each pattern is appended only once, so no duplicates.<\/li>\n<li>Called without arguments, it just prints the current list.<\/li>\n<\/ul>\n<h2>Usage<\/h2>\n<pre class=\"match-braces line-numbers\"><code class=\"language-bash\">git ignore-local omgyes &quot;*.mp3&quot;   # ignore stuff locally\ngit ignore-local                  # show what's ignored\n<\/code><\/pre>\n<p>And <code>git status<\/code> stays clean:<\/p>\n<pre class=\"match-braces line-numbers\"><code>On branch main\nYour branch is up to date with 'origin\/main'.\n\nnothing to commit, working tree clean\n<\/code><\/pre>\n<p>If you ever wonder why a file is ignored, <code>git check-ignore -v &lt;file&gt;<\/code> tells you which rule matched and in which file.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>You have files that you want to ignore, but that have no business being in the project&#39;s .gitignore: a a local test script, some draft assets. Adding them to .gitignore would mean committing your personal clutter for everyone. Leaving them alone means git status keeps nagging about untracked files. Git has a built-in answer for this, .git\/info\/exclude, but it&#39;s tucked\u2026<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_ozhmd_markdown":"You have files that you want to ignore, but that have no business being in the project's `.gitignore`: a a local test script, some draft assets. Adding them to `.gitignore` would mean committing your personal clutter for everyone. Leaving them alone means `git status` keeps nagging about untracked files.\r\n\r\nGit has a built-in answer for this, `.git\/info\/exclude`, but it's tucked away inside `.git\/` where I never look, and I tend to forget what's in it. I wanted something visible, right at the root of the repo, that works the same way in all my local repositories.\r\n\r\n## Step 1: a global excludes file\r\n\r\nGit lets you define an additional ignore file with `core.excludesfile`. Most people point it to a global file in their home directory. The trick here is to give it a **relative** path instead:\r\n\r\n```bash\r\ngit config --global core.excludesfile .gitignore-local\r\n```\r\n\r\nEvery repo now can have its own optional `.gitignore-local`, read just like a regular `.gitignore`, but never shared.\r\n\r\nThere's one catch: the `.gitignore-local` file itself shows up as untracked. The fix is to make it ignore itself, by having `.gitignore-local` as its first line:\r\n\r\n```\r\n.gitignore-local\r\n# local files you want to ignore but not share via regular .gitignore:\r\ntest-script.py\r\nassets\/img\/test-logo.png\r\n```\r\n\r\nDoing that by hand in every repo gets old fast.\r\n\r\n## Step 2: a `git ignore-local` alias\r\n\r\nSo here's an alias that takes care of it:\r\n\r\n```bash\r\ngit config --global alias.ignore-local '!f() {\r\n    file=.gitignore-local\r\n    grep -qxF \"$file\" \"$file\" 2>\/dev\/null || echo \"$file\" >> \"$file\"\r\n    [ $# -eq 0 ] && { cat \"$file\"; return; }\r\n    for p in \"$@\"; do\r\n        grep -qxF -- \"$p\" \"$file\" || echo \"$p\" >> \"$file\"\r\n    done\r\n}; f'\r\n```\r\n\r\nWhat it does:\r\n\r\n- Shell aliases (the ones starting with `!`) always run from the root of the repository, so the file lands in the right place even if you call the alias from a subdirectory.\r\n- If `.gitignore-local` doesn't exist yet, or exists without its self-ignoring line, that line is added.\r\n- Each pattern is appended only once, so no duplicates.\r\n- Called without arguments, it just prints the current list.\r\n\r\n## Usage\r\n\r\n```bash\r\ngit ignore-local omgyes \"*.mp3\"   # ignore stuff locally\r\ngit ignore-local                  # show what's ignored\r\n```\r\n\r\nAnd `git status` stays clean:\r\n\r\n```\r\nOn branch main\r\nYour branch is up to date with 'origin\/main'.\r\n\r\nnothing to commit, working tree clean\r\n```\r\n\r\nIf you ever wonder why a file is ignored, `git check-ignore -v <file>` tells you which rule matched and in which file.","footnotes":""},"categories":[21],"tags":[396],"class_list":["post-3956","post","type-post","status-publish","format-standard","hentry","category-published","tag-git"],"_links":{"self":[{"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/posts\/3956","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/comments?post=3956"}],"version-history":[{"count":1,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/posts\/3956\/revisions"}],"predecessor-version":[{"id":3957,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/posts\/3956\/revisions\/3957"}],"wp:attachment":[{"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/media?parent=3956"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/categories?post=3956"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/planetozh.com\/blog\/wp-json\/wp\/v2\/tags?post=3956"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}