Skip to content

fix(windows): resolve Claude binary path and apiKeyHelper shell syntax on Windows#137

Open
yakup-ozturk wants to merge 1 commit into
databricks:mainfrom
yakup-ozturk:fix/windows-support
Open

fix(windows): resolve Claude binary path and apiKeyHelper shell syntax on Windows#137
yakup-ozturk wants to merge 1 commit into
databricks:mainfrom
yakup-ozturk:fix/windows-support

Conversation

@yakup-ozturk

Copy link
Copy Markdown

fix(windows): resolve Claude binary path and apiKeyHelper syntax on Windows:

Three changes that make ucode claude work on Windows without manual workarounds:

  1. WinError 2 — Claude binary not found (agents/claude.py) Python's subprocess resolves PATH differently from cmd.exe and cannot find npm .cmd wrappers by name. Resolve CLAUDE_BINARY by walking from the claude.cmd wrapper to the actual claude.exe inside the npm package tree. Falls back to the wrapper path (or the bare "claude" string) if the .exe is absent, so non-Windows installs and non-standard npm layouts are unaffected.

  2. apiKeyHelper shell syntax error on Windows (databricks.py) Claude Code runs the apiKeyHelper command via cmd.exe on Windows, which rejects the POSIX [ -n "$VAR" ] / env -u / jq syntax currently emitted by build_auth_shell_command. On os.name == 'nt' the function now returns a python -c "..." one-liner that handles the DATABRICKS_BEARER short- circuit and calls the Databricks CLI with subprocess, avoiding both the bash syntax and the jq dependency. POSIX behaviour is unchanged.

  3. Settings not visible to the Claude desktop / IDE extension (agents/claude.py) ucode claude writes config to ~/.claude/ucode-settings.json and passes --settings when launching the CLI, but the Claude desktop app and IDE extensions read ~/.claude/settings.json by default. After writing the ucode-managed file, also merge the auth/env overlay into settings.json so the apiKeyHelper and ANTHROPIC_* vars are available in every launch path. A backup of the pre-existing settings.json is created alongside the existing ucode-settings backup.

Reference: https://medium.com/@Yakup-Ozturk/getting-claude-code-to-work-with-the-databricks-ai-gateway-on-windows-three-fixes-i-wish-i-knew-f6e71ece06a9

…x on Windows

Three changes that make `ucode claude` work on Windows without manual workarounds:

1. **WinError 2 — Claude binary not found** (`agents/claude.py`)
   Python's `subprocess` resolves PATH differently from cmd.exe and cannot find
   npm `.cmd` wrappers by name. Resolve `CLAUDE_BINARY` by walking from the
   `claude.cmd` wrapper to the actual `claude.exe` inside the npm package tree.
   Falls back to the wrapper path (or the bare `"claude"` string) if the .exe
   is absent, so non-Windows installs and non-standard npm layouts are unaffected.

2. **apiKeyHelper shell syntax error on Windows** (`databricks.py`)
   Claude Code runs the `apiKeyHelper` command via `cmd.exe` on Windows, which
   rejects the POSIX `[ -n "$VAR" ]` / `env -u` / `jq` syntax currently emitted
   by `build_auth_shell_command`. On `os.name == 'nt'` the function now returns
   a `python -c "..."` one-liner that handles the `DATABRICKS_BEARER` short-
   circuit and calls the Databricks CLI with `subprocess`, avoiding both the bash
   syntax and the `jq` dependency. POSIX behaviour is unchanged.

3. **Settings not visible to the Claude desktop / IDE extension** (`agents/claude.py`)
   `ucode claude` writes config to `~/.claude/ucode-settings.json` and passes
   `--settings` when launching the CLI, but the Claude desktop app and IDE
   extensions read `~/.claude/settings.json` by default.  After writing the
   ucode-managed file, also merge the auth/env overlay into `settings.json` so
   the `apiKeyHelper` and `ANTHROPIC_*` vars are available in every launch path.
   A backup of the pre-existing `settings.json` is created alongside the existing
   ucode-settings backup.

@rohita5l rohita5l left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing the Windows path here. I think the intent is right, but it requires some changes

The PR currently mixes three separate behavior changes:

  1. Windows auth command syntax.
  2. Windows Claude binary resolution.
  3. Writing ucode’s Claude overlay into the user’s default ~/.claude/settings.json.

The first two are Windows fixes. The third changes default behavior for all users and should either be opt-in or split into a separate PR. We definitely dont want as the default behavior and prefer to keep ucode settings separate.

My preferred direction:

  • Keep SPEC["binary"] = "claude" stable.
  • Resolve a Windows-specific Claude executable only at launch/validation time.
  • Keep writing ~/.claude/ucode-settings.json by default.
  • Make merging into ~/.claude/settings.json explicit opt-in
  • Add focused tests for the Windows auth command and binary resolution behavior.


SPEC: ToolSpec = {
"binary": "claude",
"binary": CLAUDE_BINARY,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not convinced we should resolve CLAUDE_BINARY at import time or on all platforms.

This makes SPEC["binary"] environment-dependent and changes macOS/Linux behavior too. In my local focused test run on this PR, tests/test_agent_claude.py failed because SPEC["binary"] became my absolute
~/.nvm/.../bin/claude path instead of "claude".

Could we scope this to Windows launch/validation only, preferably behind a helper like _resolve_claude_binary()? That would keep the default spec stable and avoid doing PATH/package-layout probing during module import.

/ "@anthropic-ai"
/ "claude-code"
/ "bin"
/ "claude.exe"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we verify this actually fixes the Windows launch failure?

My concern is that npm usually installs .cmd / .ps1 / shim wrappers, and I’m not sure @anthropic-ai/claude-code actually ships bin/claude.exe. If _claude_exe.exists() is false, this falls back to the
.cmd wrapper path. Passing a .cmd path to subprocess with shell=False can still fail on Windows because CreateProcess does not execute batch files directly.

It would be safer to either invoke the .cmd through cmd /c in the Windows path, or resolve this at the call site in a way that matches how validate_tool / launch actually execute the binary.

# finds the apiKeyHelper and ANTHROPIC_* env vars regardless of whether it
# is launched via `ucode claude` (which passes --settings) or directly from
# the Claude desktop app / IDE extension (which reads settings.json by default).
existing_default = read_json_safe(CLAUDE_DEFAULT_SETTINGS_PATH)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make writing to the default ~/.claude/settings.json opt-in?

ucode claude already launches with --settings ~/.claude/ucode-settings.json, so this PR broadens the behavior from “manage ucode’s Claude config” to “also mutate the user’s main Claude config.” That has a larger blast radius, especially because direct claude usage would start inheriting the Databricks gateway apiKeyHelper / ANTHROPIC_* settings.

A simpler shape might be: keep writing ucode-settings.json by default, and add an explicit option for users who want this global behavior

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do write to ~/.claude/settings.json, we should also wire it into cleanup/rollback.

Right now the managed config state and validation rollback are centered on SPEC["config_path"] / SPEC["backup_path"], which still point at ucode-settings.json. This adds a second managed file, but I don’t see matching restore behavior for it.

Comment thread src/ucode/databricks.py

def build_auth_shell_command(workspace: str, profile: str | None = None) -> str:
workspace_arg = shlex.quote(workspace.rstrip("/"))
workspace_clean = workspace.rstrip("/")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we split the platform-specific command construction into small helpers instead of building both shell dialects inline here?

The early return is not the main issue for me; the function now mixes POSIX shell, Windows cmd.exe, Python -c, profile handling, and DATABRICKS_BEARER fallback in one place, which makes the quoting hard to
audit.

For example:

def build_auth_shell_command(workspace: str, profile: str | None = None) -> str:
    workspace_clean = workspace.rstrip("/")
    if os.name == "nt":
        return _build_windows_auth_command(workspace_clean, profile)
    return _build_posix_auth_command(workspace_clean, profile)

That keeps the public API simple while isolating the two command syntaxes. It would also make it easier to add focused tests for Windows quoting separately from the existing POSIX sh behavior.

Comment thread src/ucode/databricks.py
"p=subprocess.run(cmd,capture_output=True,text=True,check=True,timeout=30); "
"print(json.loads(p.stdout)['access_token'])"
)
return f'"{python_exe}" -c {helper_code!r}'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This python -c command is hard to audit because it nests Python repr inside another repr, then hands the result to cmd.exe.

Values containing quotes or % can behave differently under cmd.exe, and this is exactly the kind of code that tends to regress silently. Could we either move the Windows helper into a small dedicated helper
function with explicit quoting/tests, or avoid the -c blob by invoking a stable ucode subcommand/helper instead?

@rohita5l

rohita5l commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator
  1. Instead of trying to find claude.exe, just treat Windows wrappers as wrappers.

Something like:

  def claude_command(binary: str = "claude") -> list[str]:
      if os.name == "nt":
          wrapper = shutil.which("claude.cmd") or shutil.which("claude.bat") or shutil.which(binary)
          if wrapper and wrapper.lower().endswith((".cmd", ".bat")):
              return ["cmd", "/c", wrapper]
      return [binary]

Then in launch/validation:

  cmd = claude_command(SPEC["binary"])
  os.execvp(cmd[0], [*cmd, "--settings", str(CLAUDE_SETTINGS_PATH), *tool_args])

and validation:

  return [
      *claude_command(binary),
      "--settings",
      str(CLAUDE_SETTINGS_PATH),
      "-p",
      "say hi in 5 words or less",
      "--max-turns",
      "1",
  ]
  1. For changing the global ~/.claude/settings.json, there should be a separate command or a flag in ucode configure claude

  2. Finally, keep the existing POSIX implementation unchanged, and add one Windows helper:

  def build_auth_shell_command(workspace: str, profile: str | None = None) -> str:
      workspace_clean = workspace.rstrip("/")
      if os.name == "nt":
          return _build_windows_auth_command(workspace_clean, profile)
      return _build_posix_auth_command(workspace_clean, profile)

For Windows, I’d avoid a complicated inline python -c if possible.

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.

2 participants