Skip to content

bin/devkit: fix Windows first-run install (curl+schannel bug, #58) - #59

Merged
5uck1ess merged 3 commits into
mainfrom
fix/windows-schannel-install-58
Apr 10, 2026
Merged

bin/devkit: fix Windows first-run install (curl+schannel bug, #58)#59
5uck1ess merged 3 commits into
mainfrom
fix/windows-schannel-install-58

Conversation

@5uck1ess

@5uck1ess 5uck1ess commented Apr 10, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #58. Windows Git Bash users cannot install the devkit engine on first run because curl.exe ships with the schannel TLS backend, which aborts with CURLE_WRITE_ERROR (exit 23) partway through responses from release-assets.githubusercontent.com — the CDN that github.com/.../releases/download/... 302-redirects to. The wrapper dies at the checksums.txt download step and devkit-engine is never installed, so the MCP server never starts.

Changes

bin/devkit

Replaces download_fresh and download_resume with a single download_release_asset helper that tries downloaders in order of TLS-stack reliability:

  1. gh release download — Go crypto/tls, no schannel involvement. Preferred.
  2. curl -fsSL [-C -] — unchanged Unix path; still fails on Windows schannel.
  3. wget [-c] — Linux fallback.
  4. powershell.exe Invoke-WebRequest — Windows last resort; .NET TLS stack also sidesteps the schannel bug.

The gh and PowerShell paths are non-resumable (fresh GET, --clobber overwrites any existing partial). That is acceptable because the outer checksum-verify loop in ensure_engine() already catches corruption and re-runs, so a bad retry costs at most one fresh fetch of the ~10MB engine asset.

.gitattributes (new)

Pins bin/devkit to LF. Git for Windows' default core.autocrlf=true would otherwise rewrite the wrapper with CRLF on checkout; Git Bash tolerates that today but a strict POSIX /bin/sh or a future autocrlf change could break the shebang or embedded heredocs.

Verification

  • bash -n bin/devkit — syntax OK on macOS.
  • Pending Windows verification (Tym will run from the Windows machine):
    • gh path: purge cached engine, confirm gh auth status, run bin/devkit mcp </dev/null — expect installed engine at … with no curl errors.
    • PowerShell fallback: hide gh from PATH, re-run — expect curl: (23), then devkit: trying PowerShell Invoke-WebRequest fallback, then success.
    • bash -n bin/devkit on Windows too.
  • Mac/Linux regression: no behavior change when gh is absent — the curl/wget path is identical to before.

Test plan

  • Windows Git Bash, gh installed + authed → first-run succeeds via gh release download
  • Windows Git Bash, gh hidden from PATH → curl fails, PowerShell Invoke-WebRequest fallback succeeds
  • macOS → unchanged (curl path)
  • Linux → unchanged (curl path)
  • bash -n bin/devkit passes on all three
  • .gitattributes forces LF on bin/devkit on fresh Windows clone

Notes

Not opening as draft because the change is self-contained and reviewable today; mark it draft if you'd rather wait for Windows verification to land first.

… bug

Windows curl.exe uses the schannel TLS backend, which aborts with
CURLE_WRITE_ERROR (exit 23) partway through responses from
release-assets.githubusercontent.com — the CDN that
github.com/.../releases/download/... 302-redirects to. Git Bash users
on Windows could not install the devkit engine on first run.

Replace download_fresh/download_resume with a unified
download_release_asset helper that tries, in order:

  1. gh release download  — Go crypto/tls, no schannel.
  2. curl -fsSL [-C -]    — unchanged Unix path.
  3. wget [-c]            — Linux fallback.
  4. powershell.exe Invoke-WebRequest — Windows last resort,
     .NET TLS stack also sidesteps the schannel bug.

gh and PowerShell paths are non-resumable, but the outer checksum
verification loop catches corruption and re-runs, so a bad retry
costs at most one fresh fetch of the ~10MB engine asset.

Also pin bin/devkit to LF via .gitattributes so Git for Windows'
core.autocrlf does not rewrite it with CRLF on checkout.

Fixes #58.
@5uck1ess

Copy link
Copy Markdown
Owner Author

Review — one blocker, two nits, some verified non-issues

Tested the change on Windows 11 Git Bash (MSYS2). Overall the shape is right — gh first, then curl/wget, then PowerShell on Windows, with the outer checksum-verify loop catching corruption on the non-resumable paths. But the PowerShell fallback as written is broken, and in practice it means only the gh release download path works on Windows. Users without gh installed end up in the same broken state as before this PR, just with a different error at the end of the chain.

Blocker: PowerShell fallback uses an MSYS-style path

download_release_asset passes $out (an MSYS path like /c/Users/.../devkit/bin/…partial) embedded inside a quoted -Command string. MSYS2/Git Bash only auto-converts POSIX paths to Windows paths when they appear as standalone arguments to a native .exe — paths embedded inside a quoted string are just characters, no conversion happens. PowerShell then sees /c/Users/… and treats the leading / as the current drive root, resolving it to CurrentDrive:\c\Users\….

Reproduction (Windows 11, Git Bash, PowerShell 5.1)

out="/c/Users/Public/test.txt"
url="https://raw.githubusercontent.com/git/git/master/README.md"

powershell.exe -NoProfile -Command \
    "Invoke-WebRequest -UseBasicParsing -Uri '$url' -OutFile '$out'"

Output:

Invoke-WebRequest : Could not find a part of the path 'C:\c\Users\Public\test.txt'.
    + CategoryInfo          : NotSpecified: (:) [Invoke-WebRequest], DirectoryNotFoundException
    + FullyQualifiedErrorId : System.IO.DirectoryNotFoundException,...

Exit 1, no file created. Note the C:\c\Users\... — PowerShell appended the MSYS path to the drive root as literal path segments.

Effect on the fix

For Windows users without gh installed, the fallback order becomes:

  1. curl → fails with curl: (23) (the original bin/devkit wrapper fails on Windows (Git Bash): curl schannel error 23 on checksums.txt download #58 bug)
  2. wget → not present on default Git for Windows
  3. powershellalso fails with the path bug above
  4. die "download failed"

So the PR ships a working path only for the gh-installed subset of Windows users. That's still a strict improvement over main, but the PR description and verification steps present the PowerShell path as a working last resort, which it isn't today.

Suggested fix

Convert the path with cygpath -w before handing it to PowerShell:

case "${PLATFORM-}" in
    windows-*)
        if command -v powershell.exe >/dev/null 2>&1; then
            winout=$out
            if command -v cygpath >/dev/null 2>&1; then
                winout=$(cygpath -w "$out")
            fi
            log "trying PowerShell Invoke-WebRequest fallback"
            run_downloader powershell.exe -NoProfile -Command \
                "Invoke-WebRequest -UseBasicParsing -Uri '$url' -OutFile '$winout'" \
                && return 0
        fi
        ;;
esac

cygpath ships with both Git for Windows and MSYS2, so the winout=$out fallback should never actually fire — it's just defensive in case someone runs the wrapper from a non-MSYS shell.

Verified on the same box:

winout=$(cygpath -w "$out")                     # → C:\Users\Public\test.txt
powershell.exe -NoProfile -Command \
    "Invoke-WebRequest -UseBasicParsing -Uri '$url' -OutFile '$winout'"
# exit 0, file downloaded successfully

Nits (non-blocking)

  • bash -n bin/devkit should be sh -n bin/devkit — the shebang is #!/bin/sh, and bash -n will silently accept bash-only syntax that a stricter dash-derived /bin/sh would reject. Small correctness point for the syntax-check step in the test plan.
  • Verification steps don't exercise the corruption path. The PR body cites the outer checksum-verify loop as the safety net for non-resumable downloaders, but none of the listed test cases actually trigger it. Worth adding one: after a successful install, truncate the engine binary to half its size, delete the marker files, re-run, and confirm the wrapper purges and re-downloads cleanly. Otherwise that safety net is untested code.

Verified non-issues

Checked these and they're all fine, listing so reviewers don't have to re-check:

  • CRLF → LF transition clean. The committed bin/devkit blob on fix/windows-schannel-install-58 is LF, verified via gh api .../contents/bin/devkit | base64 -d | od -c (no \r anywhere). .gitattributes + a same-commit edit did the right thing; no git add --renormalize needed.
  • Preference order is sound and matches my recommendation in bin/devkit wrapper fails on Windows (Git Bash): curl schannel error 23 on checksums.txt download #58: ghcurlwgetpowershell. gh uses Go's crypto/tls, completely sidestepping schannel.
  • --clobber is correct for the non-resumable gh path. Existing partial files get overwritten cleanly.
  • -UseBasicParsing on Invoke-WebRequest is correct — avoids the IE rendering engine dependency on older Windows images.
  • case "${PLATFORM-}" in windows-*) correctly scopes the PowerShell path to Windows and is safe against unset PLATFORM.

Open question

On a failed gh release download that writes partial bytes before erroring (network drop, auth issue), the next downloader in the chain is curl -C - with resumable=1. curl will try to resume from whatever byte offset gh left, mixing bytes from two downloaders into one file. The outer checksum-verify loop catches this on the next ensure_engine call and purges, so worst case is one extra round-trip — not a correctness bug, just worth knowing. No action needed unless reviewers think it's worth defensive rm -f "$out" between downloaders on the resumable path.

Two issues caught in PR #59 review:

1. The PowerShell fallback embedded the MSYS output path
   (/c/Users/...) inside a quoted -Command string. MSYS2/Git Bash
   only auto-converts POSIX paths to Windows paths when they appear
   as standalone arguments to a native .exe; paths inside a quoted
   string are just characters, so PowerShell saw `/c/Users/...` and
   resolved it against the current drive root as `C:\c\Users\...`,
   failing with DirectoryNotFoundException. In practice this meant
   the only working path on Windows was `gh release download` --
   users without gh hit the same broken state as before this fix.

   Convert the output path with `cygpath -w` before embedding it in
   the -Command string. cygpath ships with Git for Windows and MSYS2;
   the \`winout=\$out\` initializer is a harmless defensive fallback
   for exotic shells that lack it.

2. Drop any partial $out after a failed `gh release download` before
   falling through on the resumable path. Without this, curl -C -
   would resume from whatever byte offset gh had written and splice
   two downloaders' byte streams into one file; the outer checksum
   loop catches it on the next invocation, but the extra round-trip
   is free to avoid.
@5uck1ess

Copy link
Copy Markdown
Owner Author

Thanks — all three addressed in ccf8a2f:

Blocker: PowerShell MSYS-path bug

Fixed. Now runs cygpath -w "$out" before embedding into the -Command string:

winout=$out
if command -v cygpath >/dev/null 2>&1; then
    winout=$(cygpath -w "$out") || winout=$out
fi
…
run_downloader powershell.exe -NoProfile -Command \
    "Invoke-WebRequest -UseBasicParsing -Uri '$url' -OutFile '$winout'"

The winout=$out initializer is defensive for a shell that somehow lacks cygpath; on Git for Windows and MSYS2 the real path always wins.

Open question (gh partial bytes poisoning curl resume)

Also fixed in the same commit — rm -f "$out" after a failed gh release download so curl starts fresh instead of resuming from whatever byte offset gh happened to leave. Cheap, removes the wasted round-trip.

Nits

  • sh -n not bash -n: noted, updating the verification steps in claude-shared/current-tasks.md and the PR description now.
  • Corruption path isn't exercised: adding a step to the test plan — after a successful install, truncate the engine binary, delete the marker files (well, the checksum txt isn't kept), re-run and confirm the wrapper purges and re-downloads cleanly.

Revised Windows verification steps

  1. git fetch && git checkout fix/windows-schannel-install-58 && git pull
  2. Purge: rm -f bin/devkit-engine bin/devkit-engine-v*-* bin/devkit-checksums-v*.txt bin/*.partial
  3. gh path (with gh authed): bin/devkit mcp </dev/null → expect installed engine at …, no curl errors.
  4. PowerShell fallback path (hide gh): PATH=$(echo "$PATH" | tr ':' '\n' | grep -iv 'GitHub CLI' | paste -sd:) bin/devkit mcp </dev/null → expect curl: (23), then devkit: trying PowerShell Invoke-WebRequest fallback, then success. Confirm the installed engine path is a real Windows path (not C:\c\Users\…).
  5. Corruption recovery: truncate the installed bin/devkit-engine-v*-windows-amd64.exe to half its size, re-run step 3. Expect checksum mismatch log, partial purge, and a clean re-download.
  6. sh -n bin/devkit → exit 0.

Ready for another look.

@5uck1ess

Copy link
Copy Markdown
Owner Author

Blocker verified fixed — end-to-end on Windows 11 Git Bash

Re-tested ccf8a2f4 on the same Windows 11 / Git Bash (MSYS2) box I used for the original repro. Both issues I raised are cleanly addressed:

  • cygpath -w conversion lands exactly where needed; the defensive winout=$out initializer plus the || winout=$out on the cygpath call itself is slightly more paranoid than my suggestion and I like it better.
  • rm -f "$out" between failed gh and curl -C - resolves the splice-two-byte-streams race on the resumable path. Outer checksum loop is still the ultimate safety net, but the extra round-trip is genuinely avoided now.

Verification on the fetched branch file

sh -n bin/devkit              → PASS
bash -n bin/devkit            → PASS
head -1 bin/devkit | od -c    → #!/bin/sh\n   (LF, no \r)
grep cygpath / rm -f          → both new blocks present

End-to-end PowerShell fallback (the previously-broken path)

Reproduced with a generic public path so the test matches what a fresh first-run would do:

out="/c/Users/Public/devkit-fix-verify.txt"
url="https://raw.githubusercontent.com/git/git/master/README.md"

winout=$out
if command -v cygpath >/dev/null 2>&1; then
    winout=$(cygpath -w "$out") || winout=$out
fi
# winout=C:\Users\Public\devkit-fix-verify.txt

powershell.exe -NoProfile -Command \
    "Invoke-WebRequest -UseBasicParsing -Uri '$url' -OutFile '$winout'"
# exit 0, file created (3662 bytes)

Same invocation without the cygpath conversion (i.e. main before this commit) still fails with DirectoryNotFoundException on C:\c\Users\Public\…, confirming the fix is what flipped it.

Effective Windows fallback chain post-fix

Step Before this PR This PR (pre-fix commit) This PR (ccf8a2f4)
gh release download not attempted works works
curl fails (#58 schannel) fails (unchanged) fails (unchanged)
wget rarely present rarely present rarely present
PowerShell Invoke-WebRequest not attempted fails (MSYS path bug) works (cygpath)

So Windows users without gh installed now have a working fallback for the first time, and users with gh get the fast path they always would have. The chain does what the PR description promises.

Remaining (non-blocking) nits — still open but fine to land as-is

  • Test plan still says bash -n bin/devkit; both bash -n and sh -n happen to pass on the current wrapper, so this is cosmetic — flagging only for future edits that might introduce bashisms accidentally.
  • Corruption-path test (truncate the engine mid-install, re-run, confirm purge+re-download) is still not in the test plan. The outer verify loop is cited as the safety net for the non-resumable gh / PowerShell paths but the net itself remains untested. Worth a follow-up issue, not a blocker for this PR.

Still outstanding for sign-off

The actual first-run bin/devkit mcp </dev/null install flow on Windows from a clean bin/ directory hasn't been executed on my side yet (the above tests exercise the downloaders in isolation, not ensure_engine end-to-end). Happy to run that now if you want a final green light before merge — or if you'd rather wait until after any further review feedback, I can hold.

LGTM on the code level for the blocker + open question. 👍

@5uck1ess

Copy link
Copy Markdown
Owner Author

Correction + two more blockers found during end-to-end test

Retracting my earlier "blocker verified fixed" comment (link). That verification tested the cygpath fix for PowerShell in isolation (direct powershell.exe invocation), but didn't run the full bin/devkit mcp </dev/null first-run flow. When I did run it end-to-end on Windows 11 / Git Bash (MSYS2) from a fresh clone of ccf8a2f4, both the gh path and the PowerShell fallback path still fail — for two new reasons, both introduced by this PR's refactor. Sorry for the noise.

The good news: both bugs are small and isolated to download_release_asset, and with both fixed the full chain works end-to-end for both gh installed and gh absent.


Bug A — gh release download -O <posix-abspath> silently no-ops on Windows

Same class of bug as the PowerShell one from my previous review. gh.exe is a native Windows binary, and MSYS2's POSIX→Windows path auto-conversion only fires for top-level arguments that look like paths — not for values passed to a flag. So when download_release_asset invokes:

gh release download "$tag" -R "$owner/$repo" -p "$asset" -O "$out" --clobber

with $out = /<plugin-dir>/bin/devkit-checksums-v2.1.4.txt, gh receives the POSIX path literally, interprets the leading / as the current drive root, and silently fails to write anywhere. Exit 0, no file, no stderr. The next line in ensure_engine then dies on awk: fatal: cannot open file ....

Isolation repro

# gh 2.89.0, Windows 11, Git Bash (MSYS2)
out="$PWD/bin/devkit-checksums-v2.1.4.txt"
mkdir -p bin

# Broken: POSIX abspath
gh release download v2.1.4 -R 5uck1ess/devkit -p checksums.txt -O "$out" --clobber
echo "exit=$?"         # exit=0
find . -type f         # empty

# Broken: Windows abspath via cygpath (worth noting this DOES work via -O)
winout=$(cygpath -w "$out")
gh release download v2.1.4 -R 5uck1ess/devkit -p checksums.txt -O "$winout" --clobber
echo "exit=$?"         # exit=0
ls "$out"              # file present

# Also broken: -D <posix-abspath> without -O
gh release download v2.1.4 -R 5uck1ess/devkit -p checksums.txt -D "$PWD/bin" --clobber
echo "exit=$?"         # exit=0
find . -type f         # empty

-D and -O are mutually exclusive (gh errors with "specify only one of --dir or --output") and both share the POSIX-path bug. The relative-path variants (-O bin/name.txt) happen to work because MSYS2 doesn't mangle relative args.

Fix

Apply the same cygpath -w treatment to $out for the gh path that the existing PR already applies for the PowerShell path (and factor the conversion into a helper if you'd rather, since it's now needed in two places):

if command -v gh >/dev/null 2>&1; then
    gh_out=$_out
    if command -v cygpath >/dev/null 2>&1; then
        gh_out=$(cygpath -w "$_out") || gh_out=$_out
    fi
    if run_downloader gh release download "$_tag" \
            -R "${RELEASE_OWNER}/${RELEASE_REPO}" \
            -p "$_asset" -O "$gh_out" --clobber; then
        return 0
    fi
    log "gh release download failed; falling back to curl/wget"
    rm -f "$_out" 2>/dev/null || true
fi

($_out / $_tag / $_asset because of Bug B below.)


Bug B — download_release_asset clobbers outer $asset and $tag

POSIX /bin/sh has no local. So this inside download_release_asset:

download_release_asset() {
    tag=$1
    asset=$2
    out=$3
    resumable=$4
    url="..."
    ...
}

assigns to the global asset, tag, url, out, resumable. The old download_fresh(url, out) and download_resume(url, out) helpers only collided on names the caller didn't use; the refactor introduced asset=$2, which collides with ensure_engine's own $asset variable.

Sequence of events in the current PR:

# ensure_engine
asset="devkit-${PLATFORM}${ext}"                 # = "devkit-windows-amd64.exe"
...
download_release_asset "$tag" "checksums.txt" "$sums_file" ""
# ↑ inside the function: `asset=$2` → now global $asset = "checksums.txt"

expected=$(awk -v name="$asset" '$2 == name || $2 == "*"name { print $1; exit }' "$sums_file")
# ↑ searches for "checksums.txt" in the checksums file, finds nothing
if [ -z "$expected" ]; then
    rm -f "$sums_file" || true
    die "no checksum entry for $asset in release $tag"
fi

Observed stderr on the current PR head (patched with the gh cygpath fix from Bug A so that we get this far):

devkit: no checksum entry for checksums.txt in release v2.1.4

Fix

Rename the four function parameters with an underscore prefix and update all uses inside the function body:

download_release_asset() {
    _tag=$1
    _asset=$2
    _out=$3
    _resumable=$4
    _url="https://github.com/${RELEASE_OWNER}/${RELEASE_REPO}/releases/download/${_tag}/${_asset}"
    ...
}

Affected references inside the function: $tag, $asset, $out, $resumable, $url, plus the rm -f "$out" cleanup between failed gh and curl, and the winout=$out / gh_out=$out derivations. All need the underscore prefix.

Alternative: use a subshell wrapper ( ... ) around the function body so assignments stay local — cheaper diff but adds a fork per call; not a big deal since this is first-run only.


End-to-end verification with both fixes applied

I applied both fixes to a local clone of ccf8a2f4 and ran bin/devkit mcp </dev/null twice from a clean bin/ directory.

gh path (gh on PATH + authenticated)

devkit: first-run: downloading engine v2.1.4 (windows-amd64)…
devkit: installed engine at <plugin-dir>/bin/devkit-engine-v2.1.4-windows-amd64.exe
devkit MCP server ready

Exit 0. Engine binary: 9,979,392 bytes.

PowerShell fallback (gh hidden from PATH)

curl: (23) client returned ERROR on write of 524 bytes
devkit: trying PowerShell Invoke-WebRequest fallback
devkit: first-run: downloading engine v2.1.4 (windows-amd64)…
curl: (23) client returned ERROR on write of 16384 bytes
devkit: trying PowerShell Invoke-WebRequest fallback
devkit: installed engine at <plugin-dir>/bin/devkit-engine-v2.1.4-windows-amd64.exe
devkit MCP server ready

Exit 0. Same engine binary, same size. Both checksums.txt (524 bytes) and the engine (~10MB) fall through curl → PowerShell and succeed.

sh -n bin/devkit passes with both patches applied.


TL;DR

  • ccf8a2f4 as-is: broken on Windows (gh silent no-op on the checksums download, then variable clobber fallout even if gh somehow worked).
  • ccf8a2f4 + cygpath -w on gh -O + rename function params: works end-to-end for both fallback chains.
  • Both are small surgical changes scoped entirely to download_release_asset. Happy to open a follow-up PR into your branch if that's easier than patching locally — just say the word.

End-to-end test on Windows 11 / Git Bash (MSYS2) against ccf8a2f
exposed two more bugs in the download_release_asset refactor, both
in the same function and both blocking first-run install even after
the previous cygpath fix to the PowerShell path.

Bug A: gh release download -O <posix-abspath> silently no-ops.
  gh.exe is a native Windows binary; MSYS2 only auto-converts POSIX
  paths to Windows paths for top-level args that "look" like paths,
  not for values passed to a flag. So when we invoked
    gh release download ... -O "/<plugin-dir>/bin/checksums-vX.txt"
  gh saw the leading `/` as the current drive root, silently failed
  to write anywhere, and exited 0. The next awk lookup then died on
  an empty file.

  Fix: run $out through `cygpath -w` once up front and pass the
  converted form to both gh.exe and powershell.exe. The existing
  PowerShell path already did this; hoisting the conversion above
  the gh block means both native-Windows consumers get a real
  Windows path. Harmless on macOS/Linux where cygpath is absent
  (the shell falls back to the untouched POSIX path).

Bug B: download_release_asset clobbered caller globals.
  POSIX /bin/sh has no `local`, so the bare `tag=$1 / asset=$2 /
  out=$3 / resumable=$4` at the top of the function assigned to the
  global scope and silently rewrote ensure_engine's own $asset after
  the first call — turning "devkit-windows-amd64.exe" into
  "checksums.txt" and breaking the awk lookup in the checksum file
  with `no checksum entry for checksums.txt`.

  Fix: rename all four params (and the derived $url / $winout) with
  a `_` prefix so the function's locals no longer collide with
  caller state. Namespacing discipline rather than a subshell
  wrapper — keeps `return` semantics simple and avoids a fork on
  every call.

With both fixes applied, bin/devkit mcp </dev/null from a clean
bin/ directory succeeds end-to-end on Windows in both the gh-on-PATH
path and the PowerShell-fallback path (gh hidden).

Refs #58. Addresses review comment on PR #59.
@5uck1ess

Copy link
Copy Markdown
Owner Author

Both end-to-end blockers fixed in 91bfb39 — thanks for catching these and for the surgical repros, that made the diagnosis instant.

Bug A — gh POSIX-path silent no-op

Fixed. Hoisted the cygpath -w conversion out of the PowerShell-only block and to the top of download_release_asset as _winout, then passed that to both gh release download -O and powershell.exe. Both native-Windows consumers now get a Windows path; the curl/wget paths continue to use the untouched POSIX $_out (curl on Git Bash handles POSIX paths fine as top-level args).

_winout=$_out
if command -v cygpath >/dev/null 2>&1; then
    _winout=$(cygpath -w "$_out") || _winout=$_out
fi
…
run_downloader gh release download "$_tag" -R "" -p "$_asset" -O "$_winout" --clobber
…
run_downloader powershell.exe -NoProfile -Command \
    "Invoke-WebRequest -UseBasicParsing -Uri '$_url' -OutFile '$_winout'"

Harmless on macOS/Linux — cygpath isn't present, so _winout stays equal to $_out.

Bug B — global variable clobber

Fixed. Renamed all four function params plus the derived $url / $winout with an underscore prefix (_tag, _asset, _out, _resumable, _url, _winout). Went with the namespacing approach over your subshell alternative because it keeps return semantics simple and avoids a fork per call — and the new names document that they're locals for anyone editing this in the future.

Verified on a minimal reproduction that the outer $asset, $tag, $out, $url all survive a call to download_release_asset now:

inside: _tag=v2.1.4 _asset=checksums.txt _out=/tmp/sums.txt _url=URL/v2.1.4/checksums.txt _winout=/tmp/sums.txt
outer after call: tag=v2.1.4 asset=devkit-windows-amd64.exe out=/tmp/final url=outer-url

Other checks

  • sh -n bin/devkit → PASS
  • bash -n bin/devkit → PASS
  • Diff is contained entirely within download_release_asset; ensure_engine and everything else is untouched.

Ready for another end-to-end run on Windows. If both fallback chains come back green this should be merge-ready.

@5uck1ess
5uck1ess merged commit ed4b8c5 into main Apr 10, 2026
4 checks passed
@5uck1ess
5uck1ess deleted the fix/windows-schannel-install-58 branch April 10, 2026 20:03
5uck1ess added a commit that referenced this pull request Apr 10, 2026
Replaces the probe stub at mcpb/launcher/main.go with the real launcher
that ports bin/devkit's download/verify/exec loop to Go. Claude Code
spawns this binary directly on Windows via the MCPB bundle's
platform_overrides.win32 entry, and from there the flow mirrors the
POSIX bin/devkit wrapper end-to-end.

Per-invocation flow:
  1. Read CLAUDE_PLUGIN_ROOT from env (CC exports it to every MCP child).
     Reject if unset or non-absolute — either state indicates a spoofed
     or corrupted launch context.
  2. Parse <CLAUDE_PLUGIN_ROOT>/.claude-plugin/plugin.json for the plugin
     version.
  3. Validate the version against a conservative regex so a corrupted
     plugin.json can't inject path traversal into the engine filename
     (version gets interpolated into a filename joined to binDir). Regex
     allows semver + pre-release tags; explicit ".." / "/" / "\\" guard
     as defense in depth.
  4. Locate the engine at
     <CLAUDE_PLUGIN_ROOT>/bin/devkit-engine-v<version>-windows-amd64.exe
     (same cache location as bin/devkit, so nothing needs re-downloading
     if the user already ran the engine through the POSIX path).
  5. If missing: fetch checksums.txt + the engine asset from the matching
     GitHub release, verify SHA-256, atomic os.Rename into place.
  6. Best-effort sweep of stale devkit-engine-v* and devkit-checksums-v*
     files from other versions in binDir. Matches bin/devkit's find sweep.
  7. cmd.Start + cmd.Wait the engine with inherited stdio. Windows has
     no execve, so the launcher stays alive as the engine's parent until
     it exits. MCP JSON-RPC flows straight through CC's stdio pipes
     into the engine with zero buffering or framing on the launcher side.
     Exit code is forwarded via os.Exit(exitErr.ExitCode()).

Go stdlib only, no external modules. GOOS=windows GOARCH=amd64
CGO_ENABLED=0 static build. ~6.5MB binary — larger than the probe stub
because it now pulls in net/http, crypto/tls, encoding/json, and
regexp, but still reasonable for the amount of work it does.

Critically, Go's crypto/tls handles the HTTPS fetch — not Windows
schannel — which sidesteps the CDN renegotiation bug that PR #59 had
to work around for curl.exe on release-assets.githubusercontent.com.
So the Windows download path here is more robust than the POSIX one.

Verified on macOS via full mcpb-cache wipe + claude mcp list: the
POSIX proxy still works unchanged (server/devkit still just re-execs
$CLAUDE_PLUGIN_ROOT/bin/devkit), /mcp shows Connected, full MCP
handshake succeeds. This commit only touches the Windows code path.

Pending: end-to-end Windows verification in a real CC UI window.
The probe already proved the spawn layer (platform_overrides.win32,
${__dirname} expansion, PE spawn under .mcpb-cache, env var export).
The real launcher adds download + exec on top; those need a separate
pass to confirm the fetch + checksum path works against the live
GitHub release and the engine child JSON-RPC roundtrips correctly
through the launcher's inherited stdio.

Refs #60
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bin/devkit wrapper fails on Windows (Git Bash): curl schannel error 23 on checksums.txt download

1 participant