Use a bare thv command as the LLM token helper - #6326
Conversation
thv llm setup failed on every Windows machine: the token-helper command interpolated os.Executable() into a shell string and rejected shell metacharacters, including the backslash that every Windows path contains. The suggested remedy — move thv to a path without backslashes — cannot be followed on Windows. Emit "thv llm token" instead of quoting an absolute path. A bare command has nothing to escape, so it is valid in both /bin/sh and cmd.exe without a platform branch, and the metacharacter blocklist, its Windows handling, and the gating that let one direct-mode tool abort setup for unrelated tools all become unnecessary. The trade-off is that thv now resolves via PATH at invocation time, so a binary earlier on PATH could shadow it.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6326 +/- ##
==========================================
- Coverage 72.85% 72.85% -0.01%
==========================================
Files 742 742
Lines 77835 77807 -28
==========================================
- Hits 56710 56686 -24
- Misses 17146 17150 +4
+ Partials 3979 3971 -8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| Advisory | Module | Summary | Fixed in |
|---|---|---|---|
GO-2026-6179 |
golang.org/x/mod |
Transparency log tile verification bypass in sumdb/tlog |
toolchain 1.25.13 |
GO-2026-6180 |
golang.org/x/mod |
Ignore unrelated, unauthenticated hashes in sumdb Lookup |
toolchain 1.25.13 |
Both are toolchain advisories — the same class and root cause as the three already excluded. The existing comment in security-scan.yml anticipated this: CI's setup-go: stable lags the actual Go release, so govulncheck flags a toolchain the runner cannot yet resolve to. The list simply hasn't been updated for the latest batch, so the "unignored vulnerabilities" branch exits 1.
This should affect every open PR, not just this one.
Fix: a separate PR adding GO-2026-6179 GO-2026-6180 to IGNORED_VULNS in .github/workflows/security-scan.yml, with the same justification pattern already used there. Keeping it out of this PR per the repo's one-logical-change scope rule. Alternatively it resolves on its own once setup-go: stable advances past the fixed toolchain, though that timing is not in our control.
Caveat: the advisory IDs above come from running govulncheck locally on go1.26.0, which is older than CI's toolchain — my stdlib findings over-report and won't match CI's exactly. The two x/mod entries are toolchain-scoped and should be identical, but the authoritative list is in the job log under the "govulncheck raw output" group: https://github.com/stacklok/toolhive/actions/runs/31750729603/job/94615638842
Summary
thv llm setupfailed on every Windows machine when Claude Code or Claude Desktop was detected:We built the token helper by interpolating
os.Executable()into a shell command string ("<path>" llm token) written into Claude Code'sapiKeyHelper. Since a shell re-executes that string, we rejected paths containing shell metacharacters — including\. Backslash is the Windows path separator, so no Windows path could ever pass, and the advice the error gives is impossible to follow there. WinGet was incidental:C:\thv.exefailed identically.thv llm tokeninstead of quoting an absolute path. A bare command has nothing to escape, so it is valid in both/bin/shandcmd.exewith no platform branch.tokenHelperCommandNeeded. With no interpolation the command cannot fail, so the gating that let one direct-mode tool abort setup for unrelated proxy-mode tools is gone too.Net −109 lines.
Fixes #6325
thvnow resolves viaPATHThis is the tradeoff reviewers should focus on. The helper no longer names an absolute path, so
thvis resolved byPATHwhen the AI tool invokes it — potentially months after setup, in that tool's environment.Why we did it anyway. The alternative is quoting an absolute path into a string parsed by a different shell per platform —
/bin/shon POSIX,cmd.exevia ComSpec on Windows — and Go has no portable shell-escaping primitive. No single quoting scheme survives both. The rejected alternatives:\→/on Windowscmd.exeresolves a quoted forward-slash program path; needs a Windows box or a CI probe to confirm. Also leaves POSIX paths containing$broken.shellescape.Quotecmd.exe, so Windows stays broken.apiKeyHelperis a shell-string field with no argv form (verified in the shipped Claude Code binary:shell:!0via execa, and the settings schema types it as a shell command).Risk. A binary earlier on
PATHnamedthv/thv.execould shadow the real one. Its blast radius is bounded by what the helper does: it returns a token string on stdout and nothing else. It cannot redirect the client — the endpoint is fixed byANTHROPIC_BASE_URLin settings.json, which the helper never touches. So a shadowing binary could return a token of its choosing (causing failed auth, or auth as an identity the attacker controls) and observe the environment it is invoked in. It requires an attacker who can already write to the user'sPATH— a high bar that implies broader compromise — but it is still a genuine reduction from naming one specific file. If reviewers judge that unacceptable, the shim approach is the fallback and removes the concern without reintroducing the quoting problem.Type of change
Test plan
task test)task lint-fix)Also verified
GOOS=windows go build ./....Changes
pkg/llm/setup.gobuildTokenHelperCommand+tokenHelperCommandNeededwith thetokenHelperShellCommandconstant; drop the now-unused parameter fromconfigureDetectedToolspkg/llm/setup_test.gopkg/client/llm_gateway_credential_helper.goisSafeTokenHelperCommandto a metacharacter check instead of a shape assertionpkg/client/llm_gateway_credential_helper_test.gopkg/client/llm_gateway_test.goapiKeyHelpervalueDoes this introduce a user-facing change?
Yes.
thv llm setupnow works on Windows for Claude Code (previously it always failed). The value written toapiKeyHelperchanges from"/abs/path/to/thv" llm tokentothv llm tokenon all platforms. Existing configurations are rewritten on the nextthv llm setuprun; no migration is needed.Claude Desktop remains unsupported on Windows for an unrelated reason — its credential-helper shim is a POSIX
/bin/shscript. Tracked separately.Special notes for reviewers
Not verified on Windows. I have no access to a Windows machine. The change is argued to be correct by construction — a bare command needs no escaping in any shell — rather than empirically confirmed. Worth having the original reporter confirm end to end before we call the issue closed.
What was verified, from the shipped Claude Code binary (v2.1.228) rather than documentation:
apiKeyHelperis executed via execa withshell: true, so the field is genuinely shell-evaluated and this cannot be solved by passing argv.shell: truethroughComSpec(cmd.exe) — not Git Bash or PowerShell, which an earlier revision of this code assumed in a comment.Follow-up: Claude Desktop's POSIX-only shim still blocks Windows for that client and needs a
.cmd/PowerShell equivalent.Generated with Claude Code