Skip to content

Blank ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY in ug's Claude Code settings - #1185

Open
luisscabral wants to merge 6 commits into
databricks:mainfrom
luisscabral:fix/claude-drop-shadowing-anthropic-env
Open

luisscabral wants to merge 6 commits into
databricks:mainfrom
luisscabral:fix/claude-drop-shadowing-anthropic-env

Conversation

@luisscabral

@luisscabral luisscabral commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

ug now writes ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY as empty strings in the Claude Code settings env block it already owns, next to ANTHROPIC_BASE_URL. A credential left in the user's shell by another LLM gateway setup can no longer replace ug's credential. ug claude prints one stderr line when either variable is set.

This replaces the first version of this PR, which removed the variables from the environment at launch. Following review, the user's environment is no longer touched.

Why

Claude Code ranks ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY above apiKeyHelper and the subscription login (authentication precedence). The order is fixed: it applies even when apiKeyHelper is in OS-managed settings. ug already overrides the shell's ANTHROPIC_BASE_URL through its settings, so a shell set up for another gateway keeps its token but gets the Databricks URL. Claude Code then sends that token to the AI Gateway, retries ten times over about three minutes, and fails with:

API Error: 401 Credential was not sent or was of an unsupported type for this API.

ug auth-token is never called. ug doctor already warns about these variables, but nothing prevented the failure.

How

  • render_overlay adds both keys as "" to the env block. They become ug-owned keys like the rest of that block:
    • written to ~/.claude/ucode-settings.json;
    • mirrored into the OS-managed file, so a bare claude is covered too;
    • removed by ug revert.
  • Both the gateway and relayed setups get them, because the subscription login is outranked by these variables as well.
  • launch() no longer changes os.environ. It warns on stderr when a non-empty value is present, so headless stdout stays machine-readable. The warning points scripts that authenticate with a PAT at DATABRICKS_BEARER, which ug auth-token returns as is.
  • ug doctor reuses the same constant and wording, instead of telling users to unset the variables.
  • AGENTS.md's "Fields ug manages" table lists the two keys.

Things reviewers should know:

  • Commands Claude runs see the variables empty. Claude Code applies settings env to its own process, so its Bash tool, hooks and MCP servers see "" (checked with the Bash tool). The user's shell and other tools keep their values.
  • IT-set values are replaced. If an administrator has put ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN in the OS-managed file, ug now replaces it like any other ug-owned leaf (see the new managed-file test).
  • Probably one sudo prompt. Existing installs whose OS-managed file ug maintains will most likely see one privileged write the next time ug updates that file, because its content changes.
  • Deliberate Databricks credentials have their own overrides. DATABRICKS_BEARER, DATABRICKS_BEARER_COMMAND and --use-pat already serve that case.

Tests

  • tests/test_agent_claude.py:
    • both keys are blanked and owned, for default and relayed overlays;
    • they land in the private and OS-managed files, replacing an existing managed value, and are recorded for revert;
    • launch warns on stderr with the DATABRICKS_BEARER hint, leaves os.environ unchanged, and stays silent for empty values.
  • tests/integration/test_ug_claude_headless.py: test_ug_claude_headless_prompt_argument now runs with both variables set to a foreign key in the shell. It must still answer through the gateway, and stderr must report both as ignored. It is the Claude case of the runner's headless subset, so it also runs on Windows and the other Claude versions that subset covers.
  • tests/test_doctor.py: the check names the variables, points at DATABRICKS_BEARER and no longer says to unset them.
  • tests/test_e2e.py and tests/test_e2e_user_agent.py: these passed their token as ANTHROPIC_API_KEY alongside ug's settings, which ug now blanks, so they pass it through DATABRICKS_BEARER and the generated apiKeyHelper instead.
  • Full suite: uv run pytest --basetemp=/tmp/ugt gives 5042 passed and 12 failed. The same 12 tests fail on main at da363b2 on this machine:
    • 5 in test_e2e_user_agent.py, which time out before reaching their capture server because this machine's OS-managed Claude settings pin another gateway URL;
    • 4 in test_skills.py;
    • 3 in test_smart_router.py.
  • ruff check, ruff format --check, ty check src/ and tests/test_integration_contract.py pass.
  • Manual, Claude Code 2.1.299 on macOS, headless, against an AI Gateway with a Databricks apiKeyHelper:
    • A foreign ANTHROPIC_AUTH_TOKEN in the shell fails with the 401 above, after 11 attempts.
    • With --settings '{"env":{"ANTHROPIC_AUTH_TOKEN":"","ANTHROPIC_API_KEY":""}}' and both variables set in the shell, it answers, and the Bash tool reports both as empty.
    • A settings env value also beats the same shell variable for ANTHROPIC_BASE_URL.
    • A foreign ANTHROPIC_API_KEY alone did not break that headless run; ANTHROPIC_AUTH_TOKEN did.
  • Not run:
    • the live integration test and e2e tests, which need UCODE_TEST_WORKSPACE;
    • interactive mode;
    • Linux and Windows;
    • Claude Code versions other than 2.1.299.

This pull request and its description were written by Isaac.

Luis Cabral and others added 3 commits October 19, 2026 15:53
Claude Code ranks ANTHROPIC_AUTH_TOKEN and ANTHROPIC_API_KEY above
apiKeyHelper and the subscription login. When the user's shell still
exports one of them, for example from another LLM gateway setup,
`ug claude` hands it to Claude Code, which sends it to the AI Gateway
instead of ug's token. Every request then fails with 401 "Credential
was not sent or was of an unsupported type for this API" after ten
retries, and `ug auth-token` is never called.

`claude.launch()` now removes both variables before any launch path
(default, relayed, smart routing) starts Claude Code, and warns on
stderr when it drops a non-empty value.

Co-authored-by: Isaac <no-reply@databricks.com>
Rework the fix after review: instead of removing ANTHROPIC_AUTH_TOKEN
and ANTHROPIC_API_KEY from the environment at launch, ug now writes both
as empty strings in the settings env block it already owns, next to
ANTHROPIC_BASE_URL. Claude Code applies settings env over the shell, so
a credential left by another LLM gateway setup can no longer outrank
apiKeyHelper. Because the block is mirrored into the OS-managed file,
this also covers a bare `claude`, and `ug revert` removes the keys with
the rest of ug's fields. The user's shell is left untouched.

`ug claude` keeps a one-line stderr note when either variable is set.
The e2e and user-agent tests pass their token through DATABRICKS_BEARER,
since ug's settings now blank ANTHROPIC_API_KEY.

Co-authored-by: Isaac <no-reply@databricks.com>
@luisscabral luisscabral changed the title Drop inherited ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY when launching Claude Code Blank ANTHROPIC_AUTH_TOKEN / ANTHROPIC_API_KEY in ug's Claude Code settings Oct 20, 2026
Scripts that authenticated Claude Code with a PAT in ANTHROPIC_AUTH_TOKEN
or ANTHROPIC_API_KEY now get those values blanked by ug's settings. The
warning tells them to set DATABRICKS_BEARER, which ug's apiKeyHelper
returns as is.

Co-authored-by: Isaac <no-reply@databricks.com>
@billiam-wang billiam-wang added the ug-review Run the automated UG review label Oct 21, 2026
@billiam-wang

Copy link
Copy Markdown
Collaborator

/ug-review

Comment thread src/ucode/agents/claude.py Outdated
Comment thread src/ucode/agents/claude.py
Comment thread src/ucode/agents/claude.py
Comment thread tests/test_agent_claude.py
@github-actions

Copy link
Copy Markdown

Polly AI Review

Blocking issues

None verified.

Security vulnerabilities

None verified. The warning prints variable names, not credential values; the new warning helper does not mutate os.environ.

Non-blocking notes

  • tests/test_agent_claude.py:3243: can we register OAUTH_TOKEN with monkeypatch.delenv(..., raising=False) before launch? Otherwise the mocked token remains in the process after teardown.
  • tests/integration/test_ug_claude_headless.py:48: can we extend test_ug_claude_headless_prompt_argument with the conflicting credentials and warning assertion instead of repeating its live journey?
  • src/ucode/agents/claude.py:111: can we shorten the comment to “Blank inherited credentials so Claude uses ug’s configured authentication”?
  • tests/integration/test_ug_claude_headless.py:54: can we remove the historical “before this was handled” wording and describe the conflicting-shell-credentials scenario instead?

Approach

The approach is sound. It reuses settings ownership, managed-file composition, and revert handling rather than introducing launch-time environment mutation.

Summary

Diff size (computed): 8 files; +149 / -6 lines (155 changed text lines); 0 binary files.

Category Files Added Deleted Share of changed text lines
Tests and test support 6 121 5 81.3%
Documentation 1 1 0 0.6%
Dependencies and lockfiles 0 0 0 0.0%
Implementation / other 1 27 1 18.1%

Shares use additions + deletions, including generated text and lockfiles. Binary files have no line count. Tests include fixtures/helpers under test paths and colocated test/spec files; categories are path-based, not coverage measurements.

Changes

The change prevents inherited Anthropic credentials from overriding ug’s configured authentication. It adds empty credential settings, an actionable stderr warning, ownership documentation, focused coverage, and necessary authentication-input repairs in existing tests.

The size is proportional to the outcome. No unnecessary implementation scaffolding or dependency changes were found.

Tests

Test-by-test assessment

Static review only; no tests were executed. The PR’s reported execution results were not independently reproduced. Context was checked against the trusted checkout; the supplied base/head commit objects were unavailable there.

Test / case Behavior protected Layer Needed? Action / rationale
tests/README.md:149, tests/README.md:444 — documentation edits Documents credential and headless coverage Documentation keep Updates the coverage inventory; adds no executable cases. Relayed rendering coverage does not establish OS-managed writes.
tests/integration/README.md:415 — documentation edit Lists shell-credential coverage Documentation keep Relevant inventory update without additional setup.
tests/integration/test_ug_claude_headless.py::test_ug_claude_headless_ignores_shell_anthropic_credentials Native Claude settings override conflicting shell credentials; authentication succeeds and stdout remains structured Live integration consolidate Native precedence requires this external boundary, but the existing argument-prompt case crosses it already. Extend that case as noted above.
tests/test_agent_claude.py::TestRenderOverlay::test_blanks_shadowing_credentials_as_owned_keys — relayed=False / True Both overlay modes blank and own both credential keys Unit keep Parameterization covers distinct rendering branches at the smallest reliable layer.
tests/test_agent_claude.py::TestWriteToolConfigManagedSettings::test_blank_credentials_land_in_both_files_and_are_revertible Both files receive blanks, an existing managed credential is replaced, and ownership is recorded Component keep Exercises settings composition beyond rendering. It checks ownership, not an actual revert; existing lifecycle coverage exercises baseline restoration.
tests/test_agent_claude.py::TestClaudeLaunch::test_launch_warns_on_shadowing_credentials_and_keeps_env Warning uses stderr, preserves inherited Anthropic values, and gives alternative authentication guidance Component keep Focused launch contract; fix the demonstrated OAUTH_TOKEN teardown leak noted above.
tests/test_agent_claude.py::TestClaudeLaunch::test_relayed_launch_warns_on_shadowing_credentials Warning occurs before the relayed early return and preserves the inherited value Component keep Covers a distinct launch branch and singular warning wording; does not establish live subscription authentication.
tests/test_agent_claude.py::TestClaudeLaunch::test_launch_does_not_warn_for_empty_credentials Absent and whitespace-only credentials remain silent Component keep Meaningful negative boundary without expanding the input matrix unnecessarily.
tests/test_e2e.py::TestClaudeLaunch::test_launch_claude_per_model Existing per-model launches retain working authentication Live E2E keep Switching to the helper-compatible bearer input repairs setup invalidated by the new blank settings.
tests/test_e2e.py::TestModelProviderLaunch::_launch_claude_and_assert_success — shared helper Provider-launch cases retain working authentication E2E support keep One shared input repair supports test_launch_claude_through_provider and test_launch_claude_model_haiku_through_provider without adding scenarios.
tests/test_e2e_user_agent.py::TestClaudeUserAgent::test_user_agent_arrives_at_gateway Native Claude sends ug’s attribution header Native subprocess / local HTTP keep The overlay supplies apiKeyHelper, which consumes DATABRICKS_BEARER; the changed input preserves the existing wire-header test.
tests/test_e2e_user_agent.py::TestClaudeUserAgent::test_managed_http_header_arrives_at_gateway Native Claude sends the configured managed header Native subprocess / local HTTP keep Same necessary authentication-input repair, protecting a distinct header-policy assertion.

Existing managed-file lifecycle coverage checks write-to-revert baseline restoration. The live test uniquely checks Claude’s external authentication precedence; unit assertions alone cannot establish it. Live relayed authentication remains unverified, but no concrete defect or additional necessary coverage gap was established.

Scope

All changes support preventing inherited credentials from shadowing ug authentication. The existing E2E input changes repair setup affected by that behavior. No unrelated changes were found.


Automated review by Polly · workflow run

Luis Cabral and others added 2 commits October 21, 2026 17:10
- The launch warning always names both variables, without singular and
  plural variants.
- `ug doctor` reuses SHADOWING_CREDENTIAL_ENV_VARS and the launch
  warning's wording instead of telling users to unset the variables,
  which ug's settings now blank for Claude Code.
- Fold the shell-credentials scenario into the existing headless
  prompt-argument journey instead of a separate live test.
- Keep one launch warning test: the warning runs before the relayed
  branch. Register OAUTH_TOKEN so the warning test cleans it up.
- Shorten the constant's comment.

Co-authored-by: Isaac <no-reply@databricks.com>
@luisscabral

Copy link
Copy Markdown
Contributor Author

Polly's notes are addressed in f793068: OAUTH_TOKEN is registered with monkeypatch.delenv, the live check is folded into test_ug_claude_headless_prompt_argument (so it also runs in the runner's headless subset), the comment is shortened as suggested, and the historical docstring wording is gone.

@billiam-wang

Copy link
Copy Markdown
Collaborator

/integration-tests

@billiam-wang

Copy link
Copy Markdown
Collaborator

/integration-test

@github-actions

Copy link
Copy Markdown

1 similar comment
@github-actions

Copy link
Copy Markdown

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

Labels

ug-review Run the automated UG review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants