Skip to content

Set provider flags to 0 in Claude Code so a shell with CLAUDE_CODE_USE_BEDROCK=1 can't bypass the gateway - #1356

Open
jcampabadal-db wants to merge 4 commits into
databricks:mainfrom
jcampabadal-db:fix/es-2317318-claude-provider-env
Open

jcampabadal-db wants to merge 4 commits into
databricks:mainfrom
jcampabadal-db:fix/es-2317318-claude-provider-env

Conversation

@jcampabadal-db

@jcampabadal-db jcampabadal-db commented Oct 29, 2026 •

Copy link
Copy Markdown

What did you change, and why?

Claude Code selects its provider from the CLAUDE_CODE_USE_* flags before it reads ANTHROPIC_BASE_URL. A flag left over from a direct-provider setup (for example CLAUDE_CODE_USE_BEDROCK=1 in a shell profile) makes ug claude send every request straight to that provider instead of Unity AI Gateway, even after a successful ug configure. The customer in ES-2317318 had to export CLAUDE_CODE_USE_BEDROCK=0 by hand to make ug claude work.

ug now writes the six provider flags (CLAUDE_CODE_USE_BEDROCK, _VERTEX, _FOUNDRY, _ANTHROPIC_AWS, _ANTHROPIC_GOOGLE_CLOUD, _MANTLE) as "0". It uses the same path and rules as the credential blanks from #1287:

  • Always in ucode-settings.json, relayed launches included. The --settings overlay outranks the shell env and user settings.json, so an inherited =1 no longer wins.
  • In the managed settings file only on an explicit ug configure.
  • A true value (1/true/yes/on, case-insensitive) that someone else put in the managed file is kept, with a warning, because the managed file outranks ug's settings. A false value is replaced with "0".
  • The keys are excluded from the managed-file conflict check and skipped in admin passthrough, like the credential blanks.

ug doctor adds one check: info when a flag is set in the shell (ug ignores it), warn when the managed file has a true flag (ug cannot override it).

Files: src/ucode/agents/claude.py (CLAUDE_PROVIDER_ENV_KEYS, CLAUDE_NEUTRAL_ENV, _settle_credential_blanks loop), src/ucode/doctor.py (_check_claude_provider_env, which uses the shared claude.provider_flag_enabled), AGENTS.md (managed-fields table row), tests/README.md, and the native CI job in .github/workflows/ci.yml now also runs tests/native_claude/test_provider_env.py.

How do you know it works?

  • Unit tests: the provider flags share the credential blanks' lifecycle tests in tests/test_claude_credential_blanks.py (overlay, configure writes, plain launch, second writer, conflicts, relayed launch, skip mode, revert, passthrough), which now check both groups. Provider-only cases: a true admin value is kept with a warning, and a false value is replaced. The six flag names are listed in the test, so a flag dropped from CLAUDE_PROVIDER_ENV_KEYS fails 18 tests. tests/test_doctor.py has 5 new tests, including a regression for an unreadable managed directory.
  • Native tests: tests/native_claude/test_provider_env.py runs real claude -p against a fake server for Bedrock and Vertex. With the flag zeroed the request stays on /v1/messages; the control case without the overlay reaches the provider path. All 8 pass on the CI-pinned Claude Code 2.1.290.
  • Live check: a fresh install of this branch with Claude Code 2.1.310 against a real workspace answered through the gateway with CLAUDE_CODE_USE_BEDROCK=1 and with CLAUDE_CODE_USE_VERTEX=1 exported in the shell. ucode-settings.json shows all six flags as "0", and ug doctor shows the info line. On main, the same setup with CLAUDE_CODE_USE_BEDROCK=1 fails with a Bedrock 400 The provided model identifier is invalid.
  • ruff check, ruff format --check and ty check src/ pass. The full suite has 6 failures in test_e2e_user_agent and test_smart_router; the same tests fail on clean main in the same environment.

This pull request and its description were written by Isaac.

…K can't bypass the gateway

Claude Code selects Bedrock, Vertex, Foundry, Anthropic AWS, Anthropic Google Cloud or Mantle
from the CLAUDE_CODE_USE_* flags before it reads ANTHROPIC_BASE_URL. A flag left from a
direct-provider setup (CLAUDE_CODE_USE_BEDROCK=1) made `ug claude` send every request straight
to that provider instead of Unity AI Gateway, after a successful `ug configure` (ES-2317318).

ug now writes each flag as "0" next to the credential blanks from databricks#1287, with the same rules:
always in ucode-settings.json (relayed launches included), in the managed file only from
`ug configure`, and a true value already in the managed file is kept with a warning, since
the managed file outranks ug's settings. `ug doctor` reports a shell flag (info) and a
managed-file flag (warn).

Co-authored-by: Isaac <no-reply@databricks.com>
@jcampabadal-db jcampabadal-db changed the title Pin Claude Code provider flags to 0 so a shell CLAUDE_CODE_USE_BEDROCK can't bypass the gateway Set provider flags to 0 in Claude Code so a shell with CLAUDE_CODE_USE_BEDROCK=1 can't bypass the gateway Oct 29, 2026
@david-siqi-liu

Copy link
Copy Markdown
Collaborator

/ug-review

@david-siqi-liu

Copy link
Copy Markdown
Collaborator

/integration-tests

Comment thread src/ucode/doctor.py Outdated
if os.environ.get(name, "").strip().lower() in truthy
]
path = claude.managed_settings_path()
managed_env = read_json_safe(path).get("env") if path is not None and path.exists() else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

bug · medium
can we drop the path.exists() here? it runs outside read_json_safe's try, so a root-locked parent dir raises PermissionError and crashes ug doctor. read_json_safe already treats a missing file as {}.

Suggestion:

Suggested change
managed_env = read_json_safe(path).get("env") if path is not None and path.exists() else None
managed_env = read_json_safe(path).get("env") if path is not None else None

Comment thread src/ucode/doctor.py Outdated
managed = [
name
for name in claude.CLAUDE_PROVIDER_ENV_KEYS
if isinstance(managed_env.get(name), str) and managed_env[name].strip().lower() in truthy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

maintainability · low
claude.py already does this check in _takes_claude_off_gateway. can we expose one public helper (e.g. provider_flag_enabled(value)) and use it for both lists, so the truthy rules stay the same in both places?

@github-actions

Copy link
Copy Markdown
Contributor

Polly AI Review

Blocking issues

  • P2 — src/ucode/doctor.py:293: can we remove the outer path.exists()? On Python 3.12, a locked parent raises PermissionError and crashes ug doctor; read_json_safe already catches it. Add a denied-stat regression.

Security vulnerabilities

None identified in the changed code.

Non-blocking notes

  • tests/test_claude_provider_env.py:38: can we assert the six required flag names explicitly? Expectations derived from the production registry cannot catch an accidentally omitted flag.
  • tests/test_claude_provider_env.py:53: can we extend test_configure_writes_blanks_to_both_files and the related credential lifecycle cases instead of duplicating them? Consolidation targets are below.
  • src/ucode/agents/claude.py:254: can we replace the incident narration with “Pin direct-provider flags off so inherited shell flags cannot bypass the gateway”? The ticket reference adds change history.

Approach

The production approach follows the existing credential-neutralization path. Sharing the neutral-value map across overlay generation, managed ownership, and conflict handling avoids a separate configuration mechanism.

Summary

Diff size (computed): 8 files; +455 / -14 lines (469 changed text lines); 0 binary files.

Category Files Added Deleted Share of changed text lines
Tests and test support 4 356 0 75.9%
Documentation 1 1 0 0.2%
Dependencies and lockfiles 0 0 0 0.0%
Implementation / other 3 98 14 23.9%

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 PR prevents inherited Claude provider flags from bypassing the gateway. It extends the existing settings reconciliation, adds diagnostic advice, documents ownership, and includes unit/component tests plus native-process coverage and CI wiring. No dependencies or generated assets change.

The implementation is proportional to the goal. The main reduction opportunity is duplicated lifecycle setup and scenarios, detailed below.

Tests

Test-by-test assessment

The frozen snapshot checksum and both trusted policy files were verified. No PR code or tests were executed; reported passes and pre-existing failures remain unverified.

Test / case Behavior protected Layer Needed? Action / rationale
tests/test_claude_provider_env.py::test_overlay_zeroes_every_provider_flag_for_helper_and_relayed_launches Both overlay modes zero provider flags, register ownership, and retain gateway enablement Unit consolidate Extend tests/test_claude_credential_blanks.py::test_overlay_blanks_both_for_helper_and_relayed_launches. Assert the six required names independently of production constants.
tests/test_claude_provider_env.py::test_configure_writes_zeros_to_both_files Explicit configure writes both settings files Component consolidate Extend tests/test_claude_credential_blanks.py::test_configure_writes_blanks_to_both_files with provider-value assertions. Preserve the privileged-write assertion.
tests/test_claude_provider_env.py::test_plain_launch_does_not_add_zeros_to_a_fresh_managed_file Launch does not introduce managed provider entries Component consolidate Extend tests/test_claude_credential_blanks.py::test_plain_launch_does_not_add_blanks_to_a_fresh_managed_file; retain user-overlay assertions.
tests/test_claude_provider_env.py::test_plain_launch_after_second_writer_dropped_them_makes_no_write Launch does not repair entries another writer removed Component consolidate Extend tests/test_claude_credential_blanks.py::test_plain_launch_after_second_writer_dropped_them_makes_no_write to remove both neutral-value groups. Preserve repeated-launch assertions.
tests/test_claude_provider_env.py::test_configure_keeps_a_true_flag_someone_else_set_and_warns — 1, true, Yes, ON True admin values survive configure and warn; overlay remains zero Component keep Provider-specific classification differs from credential blanks. These cases verify classification together with reconciliation and warning behavior.
tests/test_claude_provider_env.py::test_configure_replaces_a_false_flag_without_warning — 0, false, empty string Configure normalizes false values without warnings Component keep Distinct provider contract: false values may be replaced, unlike preserved admin true values.
tests/test_claude_provider_env.py::test_plain_launch_leaves_a_true_flag_in_the_managed_file_alone Launch preserves an existing admin override Component consolidate Extend tests/test_claude_credential_blanks.py::test_plain_launch_leaves_present_blanks_and_values_alone with an admin true provider value and user-overlay assertions.
tests/test_claude_provider_env.py::test_credential_warning_is_unchanged Credential collisions still describe authentication failure rather than provider bypass Component consolidate Add the distinguishing warning assertion to tests/test_claude_credential_blanks.py::test_configure_keeps_a_value_someone_else_set_and_warns.
tests/test_claude_provider_env.py::test_conflicts_ignore_the_provider_flags Provider entries are excluded without suppressing genuine URL conflicts Unit consolidate Extend tests/test_claude_credential_blanks.py::test_conflicts_ignore_the_blanks; retain its positive conflict assertion.
tests/test_claude_provider_env.py::test_skip_mode_writes_only_the_user_file Skip mode avoids managed writes Component consolidate Extend tests/test_claude_credential_blanks.py::test_skip_mode_writes_only_the_user_file with provider zeros.
tests/test_claude_provider_env.py::test_revert_removes_zeros_from_the_managed_file Revert removes UG entries and preserves unrelated settings Component consolidate Extend tests/test_claude_credential_blanks.py::test_revert_removes_blanks_from_the_managed_file with provider assertions.
tests/test_claude_provider_env.py::test_admin_passthrough_cannot_turn_a_provider_flag_on Admin passthrough cannot replace UG-owned provider values Component consolidate Extend tests/test_claude_credential_blanks.py::test_admin_passthrough_cannot_override_the_blanks; retain unrelated-entry preservation assertions.
tests/test_claude_provider_env.py — env, _env, _zeros, _write_managed_env, imports/constants Supplies reconciliation setup and neutral-value assertions Test support consolidate Reuse the credential scenario setup when combining lifecycle cases; the existing _managed_env harness already supplies reconciliation infrastructure.
tests/native_claude/test_provider_env.py::test_zeroed_flag_keeps_the_turn_on_the_gateway — Bedrock, Vertex Real Claude settings zeros override inherited provider flags and retain gateway routing Native process/local HTTP keep Establishes actual provider selection and settings precedence, which mocked configuration assertions cannot prove.
tests/native_claude/test_provider_env.py::test_control_shell_flag_sends_the_turn_to_the_provider — Bedrock, Vertex Without neutralization, the inherited flag selects the provider route Native process/local HTTP keep Controls show that the positive cases are sensitive to the original routing failure.
tests/native_claude/test_provider_env.py — _ZEROS, _PROVIDERS, _no_managed_settings, _fake_server, _turn, _STREAM import Isolates the process and observes provider-specific requests Test support keep Provider-route observations differ from credential-header checks. Existing stream data and binary fixtures are reused; managed-settings isolation makes precedence deterministic.
tests/test_doctor.py::TestClaudeProviderEnv.test_none_when_unset No diagnostic for absent flags Unit keep Covers the inactive, missing-managed-file path.
tests/test_doctor.py::TestClaudeProviderEnv.test_false_values_are_ignored False shell and managed values produce no diagnostic Unit keep Distinct from absence: verifies false-value classification in both sources.
tests/test_doctor.py::TestClaudeProviderEnv.test_notes_a_shell_flag_that_ug_overrides Shell flags produce informational advice Unit keep Protects severity and explanation for an override UG can neutralize.
tests/test_doctor.py::TestClaudeProviderEnv.test_warns_on_a_managed_flag_that_ug_cannot_override Managed true values take warning precedence over shell flags Unit keep Protects the ownership boundary and diagnostic priority.
tests/test_doctor.py::TestClaudeProviderEnv._isolate, added imports Isolates inherited flags and supplies the managed-settings path Test support keep Focused setup with explicit inputs and no privileged writes.
tests/README.md:77 — coverage documentation Explains component/native coverage boundaries Documentation keep Documents the added coverage without implying live gateway validation. Update references if scenarios are consolidated.
.github/workflows/ci.yml:90 — native test selection Includes the new native module in CI CI support keep Native tests are excluded from ordinary collection, so explicit selection is necessary.

Missing coverage: add a doctor caller regression where the managed path’s existence check raises PermissionError. Existing tests/test_config_io.py::TestReadSafePermissionDenied.test_read_json_safe_returns_empty_on_permission_error protects the reader, but not this new unsafe pre-check.

When consolidating, extend the existing relayed-launch and configure → launch rewrite → revert scenarios in tests/test_claude_credential_blanks.py with provider assertions. Existing managed-file permission-failure and external-edit tests cover unchanged persistence machinery; another provider-specific failure matrix is unnecessary.

To verify after fixing the caller, run in a secret-free checkout with dependencies installed:

uv run --no-sync pytest tests/test_claude_provider_env.py tests/test_claude_credential_blanks.py tests/test_doctor.py tests/test_config_io.py -q

Scope

All changed files support the stated provider-neutralization outcome, including diagnostics, ownership documentation, and CI coverage. No unrelated changes or unresolved scope questions were found.


Automated review by Polly · workflow run

jcampabadal-db and others added 3 commits October 29, 2026 16:08
…re the provider-flag check

- doctor: remove the `path.exists()` guard before read_json_safe. On Python 3.12 it raises
  PermissionError when the managed settings directory is not readable, and ug doctor crashed.
  read_json_safe already reads a missing or unreadable file as {}. Add a regression test that
  makes Path.stat raise PermissionError.
- claude: add provider_flag_enabled(value) and use it in _takes_claude_off_gateway and in the
  doctor check, so both use the same truthy rule.
- tests: list the six provider flag names in test_claude_provider_env.py instead of taking them
  from CLAUDE_PROVIDER_ENV_KEYS, so a flag dropped from that tuple fails the tests.
- claude: remove the ticket reference from the provider-flag comment.

Co-authored-by: Isaac <no-reply@databricks.com>
…ude_credential_blanks.py

The provider flags use the same write path and rules as the credential blanks, so the lifecycle
tests now check both groups together (NEUTRAL = blanks + zeros): overlay, configure writes, plain
launch, second writer, conflicts, relayed launch, skip mode, revert, and admin passthrough. The
admin-value test also covers a true provider flag, and the credential-warning test checks that the
provider-flag text is not used. The two provider-only cases stay as separate tests: a true flag is
kept with a warning, and a false flag is replaced. tests/test_claude_provider_env.py is removed and
tests/README.md describes the merged file.

Co-authored-by: Isaac <no-reply@databricks.com>
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