Found while fixing the $HOME half of the same branch on #385 (c4ba8e9), and deliberately not fixed there — see "Why not in #385" below. Recorded in _shield_home_git_ignore's docstring so the branch is not read as complete.
The defect
When core.excludesFile and XDG_CONFIG_HOME are both unset, the shield reproduces git's fallback to $HOME/.config/git/ignore and copies that file's patterns into the worktree's private exclude — it has to, because the worktree-scoped core.excludesFile it then activates SHADOWS the operator's global one (git resolves the key from the most specific scope and never concatenates).
c4ba8e9 made the $HOME part correct on every platform by asking git rather than Python. But $HOME/.config/git/ignore is the whole of the fallback only upstream. Git for Windows patches xdg_config_home_for in its fork to prefer %APPDATA%/Git/<file> whenever that file exists:
home = getenv("HOME");
if (home && *home)
home_config = mkpathdup("%s/.config/%s/%s", home, subdir, filename);
#ifdef WIN32
{
const char *appdata = getenv("APPDATA");
if (appdata && *appdata) {
char *appdata_config = mkpathdup("%s/Git/%s", appdata, filename);
if (file_exists(appdata_config)) {
if (home_config && file_exists(home_config))
warning("'%s' was ignored because '%s' exists.", home_config, appdata_config);
free(home_config);
return appdata_config;
}
free(appdata_config);
}
}
#endif
return home_config;
— git-for-windows/git, path.c:1584-1616 at v2.55.0.windows.3.
Note git itself warns that the $HOME file "was ignored because" the APPDATA one exists: git considers these two genuinely alternative locations, not a search path.
Version bounds (counted APPDATA in path.c)
| ref |
occurrences |
git-for-windows/git v2.55.0.windows.3 |
1 |
git-for-windows/git v2.46.0.windows.1 |
1 |
git-for-windows/git v2.45.0.windows.1 |
0 |
git-for-windows/git v2.20.0.windows.1 |
0 |
git/git master (upstream) |
0 |
So: Git for Windows only, from 2.46 onward, never upstream.
Harm
Silent, and it is #384's own harm. An operator on a current Git for Windows who keeps their global ignores at %APPDATA%\Git\ignore gets:
_shield_home_git_ignore returns $HOME/.config/git/ignore, which typically does not exist;
is_file() is false, so the seed is empty — no fault, no reason, reason is None;
- the shield activates a worktree-scoped
core.excludesFile that shadows the file git really reads.
Everything they globally ignore becomes visible to git add -A inside the worktree, and the unit's commit path (verify.commit_story, and the finalize path, both unconditional git add -A) sweeps it into the story commit.
The mirror direction matters too: if BOTH files exist, git uses APPDATA and we would seed the $HOME one — copying patterns git is not applying, so the worktree over-ignores and story files can go unstaged.
Why not in #385
- It is a downstream fork's behavior, not git's — a different claim from everything else that branch verified.
- It is gated at 2.46, far above the shield's own 2.20 floor, so a naive
if APPDATA exists, prefer it would over-ignore on older Git for Windows. A correct fix needs a version gate too.
- There is no way to measure it from a POSIX box, which is that branch's standing bar for a git claim. Windows CI cannot validate it either: the runners have no
%APPDATA%\Git\ignore, so CI can only show nothing broke, not that the preference is right.
Fix shape
In _shield_home_git_ignore (src/bmad_loop/install.py), before falling back to the ~ probe:
if sys.platform == "win32" and _git_version_at_least(<version>, (2, 46)):
appdata = os.environ.get("APPDATA")
if appdata:
candidate = Path(appdata) / "Git" / "ignore"
if candidate.is_file():
return candidate
The is_file() precondition is load-bearing and mirrors git's own file_exists — the preference applies only when the file is actually there.
Two caveats for whoever picks this up:
- The version gate needs a
git version reading in this function, which currently has none. _git_version_at_least already exists; check it parses 2.46.0.windows.1 (a four-component version string) before relying on it.
- A test can pin our selection logic by faking
APPDATA and sys.platform, but it cannot prove Git for Windows agrees. Quote the fork source in the test docstring and say so, rather than implying it was measured.
Provenance
Source-read of git-for-windows/git and git/git via raw.githubusercontent.com, version bounds counted per tag. Not measured on a Windows machine — no runtime observation of Git for Windows was possible.
Found while fixing the
$HOMEhalf of the same branch on #385 (c4ba8e9), and deliberately not fixed there — see "Why not in #385" below. Recorded in_shield_home_git_ignore's docstring so the branch is not read as complete.The defect
When
core.excludesFileandXDG_CONFIG_HOMEare both unset, the shield reproduces git's fallback to$HOME/.config/git/ignoreand copies that file's patterns into the worktree's private exclude — it has to, because the worktree-scopedcore.excludesFileit then activates SHADOWS the operator's global one (git resolves the key from the most specific scope and never concatenates).c4ba8e9made the$HOMEpart correct on every platform by asking git rather than Python. But$HOME/.config/git/ignoreis the whole of the fallback only upstream. Git for Windows patchesxdg_config_home_forin its fork to prefer%APPDATA%/Git/<file>whenever that file exists:—
git-for-windows/git,path.c:1584-1616atv2.55.0.windows.3.Note git itself warns that the
$HOMEfile "was ignored because" the APPDATA one exists: git considers these two genuinely alternative locations, not a search path.Version bounds (counted
APPDATAinpath.c)git-for-windows/gitv2.55.0.windows.3git-for-windows/gitv2.46.0.windows.1git-for-windows/gitv2.45.0.windows.1git-for-windows/gitv2.20.0.windows.1git/gitmaster(upstream)So: Git for Windows only, from 2.46 onward, never upstream.
Harm
Silent, and it is #384's own harm. An operator on a current Git for Windows who keeps their global ignores at
%APPDATA%\Git\ignoregets:_shield_home_git_ignorereturns$HOME/.config/git/ignore, which typically does not exist;is_file()is false, so the seed is empty — no fault, no reason,reason is None;core.excludesFilethat shadows the file git really reads.Everything they globally ignore becomes visible to
git add -Ainside the worktree, and the unit's commit path (verify.commit_story, and the finalize path, both unconditionalgit add -A) sweeps it into the story commit.The mirror direction matters too: if BOTH files exist, git uses APPDATA and we would seed the
$HOMEone — copying patterns git is not applying, so the worktree over-ignores and story files can go unstaged.Why not in #385
if APPDATA exists, prefer itwould over-ignore on older Git for Windows. A correct fix needs a version gate too.%APPDATA%\Git\ignore, so CI can only show nothing broke, not that the preference is right.Fix shape
In
_shield_home_git_ignore(src/bmad_loop/install.py), before falling back to the~probe:The
is_file()precondition is load-bearing and mirrors git's ownfile_exists— the preference applies only when the file is actually there.Two caveats for whoever picks this up:
git versionreading in this function, which currently has none._git_version_at_leastalready exists; check it parses2.46.0.windows.1(a four-component version string) before relying on it.APPDATAandsys.platform, but it cannot prove Git for Windows agrees. Quote the fork source in the test docstring and say so, rather than implying it was measured.Provenance
Source-read of
git-for-windows/gitandgit/gitviaraw.githubusercontent.com, version bounds counted per tag. Not measured on a Windows machine — no runtime observation of Git for Windows was possible.