Repository navigation
Windows checkouts write nested hook shims with CRLF, which breaks them on Linux (the .gitattributes half of #3032 never landed) #3110
Description
What happens
Git for Windows sets core.autocrlf=true in its system gitconfig. gstack's .gitattributes pins bin/* to LF, but a pattern that contains a slash is anchored to the repository root, so it covers only the top-level bin/. On upstream main (1.91.68, 20eb6202), 15 tracked shell entry points have no eol rule and check out with CRLF on Windows:
autoplan/bin/phase-publication-hookplan-ceo-review/bin/mode-handoff-hookhosts/claude/hooks/:auq-error-fallback-hook,memorable-user-prompt-hook,question-log-hook,question-preference-hook,timeline-stop-hookbrowse/bin/find-browse,browse/bin/remote-sluglib/cso/images/:entrypoint,postgresql-ready,run-app,run-postgresqlscripts/app/gstack-browserbrowse/test/fixtures/mock-claude/claude(a test fixture)
Git Bash accepts CRLF, so nothing fails on Windows itself. The failure appears when a Linux bash runs the bytes of a Windows checkout. In my setup the gstack tree is synced from a Windows 11 machine to an Ubuntu 24.04 server, and Claude Code runs from it on both. On the Linux side:
$ printf '%s' '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | bash autoplan/bin/phase-publication-hook
autoplan/bin/phase-publication-hook: line 3: set: pipefail: invalid option name
$ echo $?
2
The hook prints nothing and exits 2. A PreToolUse hook that exits 2 blocks the call, so every /autoplan phase entry is denied on that host. test/autoplan-publication-hook.test.ts there: 11 pass, 6 fail (the hook-transport tests). I have not tried WSL on a Windows-drive checkout or a container with a bind mount, but both would read the same bytes.
bin/gstack-hook-check does not catch this, because it runs on the machine that makes the checkout. Git Bash's bash -n (5.2.37) accepts these CRLF shims, so the gate passes on Windows. Linux bash -n rejects some of them (phase-publication-hook and remote-slug fail with a syntax error; timeline-stop-hook and question-log-hook parse), but the gate never runs on the Linux side of a shared checkout.
Existing installs keep their CRLF copies
Seven more scripts already have an LF rule (*.sh, *.ts, **/scripts/*) but were still CRLF in my 1.91.19 install, with no content change. Most likely they were checked out before those rules existed: git pull never rewrites a file whose blob did not change. So a .gitattributes change alone does not repair an existing install.
The fix was already written in #3032
#3032 added these rules and scripts/heal-eol.sh for stale copies. The Oct 6 fix wave rewrote #3032 as the lean parse gate (#3057, v1.91.32.0), and the EOL half was dropped. I suggest landing that half:
hosts/*/hooks/* text eol=lf
**/bin/* text eol=lf
lib/cso/images/* text eol=lf
scripts/app/gstack-browser text eol=lf
browse/test/fixtures/mock-claude/* text eol=lfThe first four lines have #3032's patterns and attributes. The fifth covers the one fixture they miss. With all five, git check-attr --source=origin/main eol reports lf for every tracked file on main that starts with #!. The patterns match 157 files on main (141 on 1.91.19), all LF text with no binary attribute.
One gap in #3032's heal script: after it rewrites a file, the index still records the CRLF copy's size. Git treats a size change as a modification without re-hashing, so git update-index --refresh cannot clear it, and the healed files stay "modified" (a /gstack-upgrade autostash would then carry them). Re-staging each healed path with git update-index -- <path> stores the same blob and clears it.
What I run in the meantime
The five rules in .git/info/attributes, which is per clone, so upgrades never conflict with it. After each upgrade, a heal step uses #3032's method (git cat-file --filters into a cp -p temp copy, then one mv), touches only unmodified tracked files, and re-stages them as above, checking that each index entry keeps its blob.
Results on the Linux host after the heal (21 files rewritten, the same blobs in the index):
bash autoplan/bin/phase-publication-hookwith a non-Read input prints one JSON line ({}) and exits 0.test/autoplan-publication-hook.test.ts: 17 pass, 0 fail (was 11 pass, 6 fail).- The 65 test files that touch these scripts: 1,755 pass, 201 fail (was 1,619 pass, 337 fail). By test name, 136 tests are fixed and none newly fail. The fixed groups include
timeline-stop-hook, thequestion-log-hookandquestion-preference-hooksuites, thegstack-slugandremote-slugparity tests, and the v1.1.3.0 migration guard.
Environment
- Windows 11, Git for Windows 2.53.0 (
core.autocrlf=truefromC:/Program Files/Git/etc/gitconfig), gstack 1.91.19. - Ubuntu 24.04.4, bash 5.2.21, bun 1.3.14, same tree through Syncthing.
- Checked against upstream
main1.91.68 (20eb6202): same.gitattributes, same files affected, plus the newplan-ceo-review/bin/mode-handoff-hook.
Related, but a different defect: #3088 and #3100 (MSYS path spelling in gstack-hook-check).
Activity
v1.91.90.0 fix(windows): LF for nested scripts with a setup CRLF repa…
capy-ai commentedon Oct 11, 2026
Fixed in v1.91.90.0 (6e25040, #3142).
.gitattributesnow pins nestedbin/*, the hook shims,lib/cso/images/*andscripts/app/*to LF../setupalso repairs files an older Windows checkout left with CRLF: it re-checks them out in one batch and leaves locally edited files alone with a warning (docs/troubleshooting.md#setup-crlf-repair). Thanks @Mr-B-1 for tracing it back to the missing half of #3032. You're credited as co-author on the merged commit.v1.91.90.0 fix(windows): LF for nested scripts with a setup CRLF repa…