Skip to content

Use a bare thv command as the LLM token helper - #6326

Merged
jerm-dro merged 1 commit into
mainfrom
jerm-dro/fix-llm-setup-windows-path
Aug 14, 2026
Merged

Use a bare thv command as the LLM token helper#6326
jerm-dro merged 1 commit into
mainfrom
jerm-dro/fix-llm-setup-windows-path

Conversation

@jerm-dro

@jerm-dro jerm-dro commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

thv llm setup failed on every Windows machine when Claude Code or Claude Desktop was detected:

Error: executable path "C:\Users\crux\AppData\Local\...\thv.exe" contains shell-unsafe
characters; move thv to a path without quotes, backslashes, semicolons, dollar signs,
or backticks (Windows paths are not supported by thv llm setup)

We built the token helper by interpolating os.Executable() into a shell command string ("<path>" llm token) written into Claude Code's apiKeyHelper. 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.exe failed identically.

  • Emit the constant 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 with no platform branch.
  • Delete the metacharacter blocklist and 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.
  • Relax the Claude Desktop shim's validator to a plain metacharacter check. It previously pinned the exact quoted-path shape, coupling that writer to the producer's formatting.

Net −109 lines.

Fixes #6325

⚠️ Key decision: thv now resolves via PATH

This is the tradeoff reviewers should focus on. The helper no longer names an absolute path, so thv is resolved by PATH when 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/sh on POSIX, cmd.exe via ComSpec on Windows — and Go has no portable shell-escaping primitive. No single quoting scheme survives both. The rejected alternatives:

Approach Why not
Normalize \/ on Windows Was the first attempt. Rests on an unverified assumption that cmd.exe resolves a quoted forward-slash program path; needs a Windows box or a CI probe to confirm. Also leaves POSIX paths containing $ broken.
shellescape.Quote Correct for POSIX only. Single-quoting is meaningless to cmd.exe, so Windows stays broken.
Pass argv instead of a shell string Not available. apiKeyHelper is a shell-string field with no argv form (verified in the shipped Claude Code binary: shell:!0 via execa, and the settings schema types it as a shell command).
Write a shim, reference by path Works, but is strictly more code and duplicates what the Claude Desktop shim already does.

Risk. A binary earlier on PATH named thv/thv.exe could 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 by ANTHROPIC_BASE_URL in 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's PATH — 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

  • Bug fix

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing on Windows — not done; see below

Also verified GOOS=windows go build ./....

Changes

File Change
pkg/llm/setup.go Replace buildTokenHelperCommand + tokenHelperCommandNeeded with the tokenHelperShellCommand constant; drop the now-unused parameter from configureDetectedTools
pkg/llm/setup_test.go Remove tests for the two deleted functions; update call sites
pkg/client/llm_gateway_credential_helper.go Relax isSafeTokenHelperCommand to a metacharacter check instead of a shape assertion
pkg/client/llm_gateway_credential_helper_test.go Cover the bare-command form; add command-substitution rejection cases
pkg/client/llm_gateway_test.go Update expected apiKeyHelper value

Does this introduce a user-facing change?

Yes. thv llm setup now works on Windows for Claude Code (previously it always failed). The value written to apiKeyHelper changes from "/abs/path/to/thv" llm token to thv llm token on all platforms. Existing configurations are rewritten on the next thv llm setup run; no migration is needed.

Claude Desktop remains unsupported on Windows for an unrelated reason — its credential-helper shim is a POSIX /bin/sh script. 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:

  • apiKeyHelper is executed via execa with shell: true, so the field is genuinely shell-evaluated and this cannot be solved by passing argv.
  • The settings schema types it as a shell command string with no argv variant.
  • Windows resolves shell: true through ComSpec (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

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.
@github-actions github-actions Bot added size/S Small PR: 100-299 lines changed and removed size/S Small PR: 100-299 lines changed labels Aug 13, 2026
@codecov

codecov Bot commented Aug 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.85%. Comparing base (374c306) to head (9a726ca).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jerm-dro

Copy link
Copy Markdown
Collaborator Author

Go Vulnerability Check failure is unrelated to this PR

This PR changes five .go files and no dependencies — go.mod and go.sum are byte-identical to main — so govulncheck's input is unchanged by it. The branch is also already based on origin/main HEAD (374c30688), so a rebase is a no-op and would not clear it.

Cause: the workflow's IGNORED_VULNS list has gone stale. It currently covers GO-2026-5037 5038 5039 5932, but two new advisories are now reported with called symbols and are not on the list:

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

@jerm-dro
jerm-dro merged commit c503322 into main Aug 14, 2026
48 of 49 checks passed
@jerm-dro
jerm-dro deleted the jerm-dro/fix-llm-setup-windows-path branch August 14, 2026 03:32
@github-actions github-actions Bot mentioned this pull request Aug 14, 2026
2 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/S Small PR: 100-299 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

thv llm setup always fails on Windows: "shell-unsafe characters" in executable path

2 participants