Skip to content

Add a Claude Code mod so ug --header reaches Claude - #1240

Open
andy-xu-db wants to merge 5 commits into
mainfrom
andy/claude-header-mod
Open

andy-xu-db wants to merge 5 commits into
mainfrom
andy/claude-header-mod

Conversation

@andy-xu-db

@andy-xu-db andy-xu-db commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #1248 (merged), which added ug --header / UG_HEADERS for Codex and ug's own requests. This PR makes the same headers reach Claude Code:

ug --header "x-databricks-traffic-id: testenv://liteswap/<pod>" claude

Why a mod

Claude takes ANTHROPIC_CUSTOM_HEADERS whole from the highest-precedence settings. That's managed-settings.json whenever ug or Isaac writes the key there, so a header in ug's own --settings file or in the environment never reaches the gateway. Writing the header into the managed file instead would make it machine-wide, require sudo on every launch, and race with every other session and with Isaac.

A Claude mod can change ANTHROPIC_CUSTOM_HEADERS in its own process with $.env.set, and Claude reads that variable again on every request. The built-in guard refuses this change from a user's mod when managed settings set the variable. It allows it from an organization's mod: one enabled in managed settings, loaded in place from a root-owned directory marketplace, and listed in prependPlugins.

What changes

  • Mod (agents/claude_header_mod/register.ts): on session.start, and before every request (turn.step), it adds the session's UG_HEADERS lines to ANTHROPIC_CUSTOM_HEADERS. A header name already present keeps its value, so a launch can't replace ug's or an admin's headers. Malformed and repeated lines are skipped, for launches that don't go through ug. Nothing is written to disk.
  • Install: ug configure, and interactive launches, write the mod's four files to /etc/claude-code/ucode-plugins/ on Linux or /Library/Application Support/ClaudeCode/ucode-plugins/ on macOS. The writes go through the existing sudo worker, and the exact paths are added to its allowlist in both managed_files.py and managed_replace.sh. A file is only rewritten when its contents change. The sudo script now sets umask 022. Before, the folders mkdir -p created took the caller's umask, and under umask 027 the mod was unreadable.
  • Managed settings: adds extraKnownMarketplaces.ucode-managed, enabledPlugins["ug-headers@ucode-managed"], and puts the mod first in prependPlugins.
    • An admin's existing list is kept. An unset list becomes [mod, "sec-default@builtin"], so the built-in guard stays loaded.
    • The files are installed first, and the entries are added only when that install succeeded, so a failed install never enables a missing plugin. A run that can't write as root leaves the managed file as is.
    • These entries are owned for revert but never count as conflicts.
  • Revert: ug revert removes the entries and the mod's root-owned ucode-plugins folder. When an admin has extended prependPlugins, it keeps sec-default@builtin.
  • Launch: if UG_HEADERS is set but the mod isn't active (Windows, --user / UG_SKIP_OS_SETTINGS, or ug was never configured interactively), the launch warns.
  • Docs: README, the AGENTS.md fields table, and tests/README.md.

Not covered

  • Windows.
  • Whether Isaac's rewrites of the managed file keep the plugin entries. I haven't verified this.

Security review: this installs an organization-tier mod, which the guard lets change any variable that managed settings set. The mod only adds header lines and never replaces an existing name.

Testing

  • tests/test_claude_header_mod.py, which goes through the real reconcile flow:
    • install and enable
    • no rewrite when nothing changed
    • an admin's prependPlugins is kept
    • revert restores the admin's file, removes the mod's files and folders, and keeps the guard when an admin extended the list
    • a failed mod-file install leaves the plugin disabled
    • header_mod_active needs every file
    • the mod files match the sudo allowlist
    • an unreadable mod folder counts as inactive
  • tests/test_managed_files.py: the real shell script, run with umask 027 and two missing folders, creates both with mode 0755. This test fails on the old script.
  • native_claude/test_mod.py with header_mod_tests/: claude plugin validate plus the mod's merge tests under claude plugin test (Claude 2.1.304).
  • Full unit suite: 5472 passed. test_e2e_user_agent is excluded because it fails on this dev box, where managed settings override its capture server's URL.
  • Manual end-to-end run: real Claude 2.1.304 in a simulated terminal, with /etc/claude-code replaced by a tmpfs in a private mount namespace, using ug-generated mod files and a managed file that sets ANTHROPIC_CUSTOM_HEADERS. In every run the mod loaded with tier prepend and the guard refused nothing.
    • A valid UG_HEADERS completed a tool-calling turn.
    • An invalid header name failed with header 3 of 3 parsed from ANTHROPIC_CUSTOM_HEADERS, so the managed headers plus the mod's header were sent.
    • An empty value left the headers alone.
    • This didn't run the real ug configure with sudo, and it didn't check the header on the gateway side.

This pull request and its description were written by Isaac.

E2E Test:

Screen.Recording.2026-10-22.at.5.59.09.PM.mov

@andy-xu-db
andy-xu-db changed the base branch from main to andy/ug-headers October 22, 2026 20:04
@andy-xu-db
andy-xu-db force-pushed the andy/claude-header-mod branch from 64dd8c3 to 0bb5282 Compare October 22, 2026 20:04
@andy-xu-db andy-xu-db changed the title Add ug --header for per-session Claude Code gateway headers Add a Claude Code mod so ug --header reaches Claude Oct 22, 2026
@andy-xu-db

Copy link
Copy Markdown
Collaborator Author

/ug-review

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

Copy link
Copy Markdown
Contributor

Polly AI Review

Blocking issues

One blocking security issue is detailed below.

Security vulnerabilities

  • [P1] can we preserve the default guard when reverting an admin-edited plugin list? — src/ucode/agents/claude.py:1876

    With prependPlugins initially absent, configure records [ug-headers@ucode-managed, sec-default@builtin]. If an admin then appends acme@x, revert removes both recorded additions and leaves [acme@x].

    Under the plugin semantics this implementation relies on, that explicit list disables the previously implicit security guard—even though the admin retained it. Preserve sec-default@builtin when retaining subsequent admin additions. Extend tests/test_claude_header_mod.py::test_admin_prepend_list_is_kept_and_revert_restores_it with this transition.

Non-blocking notes

  • can header_mod_active() check the marketplace and prepend entry too? src/ucode/agents/claude.py:1906 can suppress the warning when a null marketplace made enablement skip.

  • can tests/native_claude/header_mod_tests/register.test.ts reset the managed headers and invoke turn.step? None of its cases exercises the per-request restoration.

  • can component tests cover the inactive-mod launch warning and failed installation? The new branches at src/ucode/agents/claude.py:2406 and src/ucode/agents/claude.py:1884 lack assertions.

  • can _managed_env reuse the setup behind _sudo_counting_env in tests/test_agent_claude.py? tests/test_claude_header_mod.py:13 largely duplicates that isolation.

  • can the generated-file equality assertion join the existing managed-path test? The allowlist subset assertion in tests/test_claude_header_mod.py:90 derives both sides from the same constants.

Approach

The organization-tier mod is consistent with the stated managed-environment restriction, and reusing the privileged writer is appropriate. Installing the files before enabling their managed entries would simplify partial-failure handling.

Summary

Diff size (computed): 13 files; +378 / -16 lines (394 changed text lines); 0 binary files.

Category Files Added Deleted Share of changed text lines
Tests and test support 6 188 7 49.5%
Documentation 2 10 4 3.6%
Dependencies and lockfiles 0 0 0 0.0%
Implementation / other 5 180 5 47.0%

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 adds a launch-local header-merging mod, privileged installation, managed plugin registration, and an inactive-mod warning. It also updates the sudo allowlists, directory-creation permissions, test support, and documentation. No dependencies or generated assets change.

The implementation is proportional to the intended outcome. The concrete reduction opportunity is duplicated test setup and allowlist assertions, rather than removing a functional layer.

Tests

Test-by-test assessment

Static review only; no tests were executed. The frozen snapshot checksum matched, and both required trusted policy files were read in full. The relevant reconciliation and privileged-worker context matched the diff’s base blobs.

Test / case Behavior protected Layer Needed? Action / rationale
tests/test_claude_header_mod.py::_managed_env Isolates privileged writes and supplies a real reconciliation environment Fixture/helper consolidate Reuse the existing _sudo_counting_env setup, extending its explicit inputs where necessary.
tests/test_claude_header_mod.py::test_configure_installs_and_enables_mod_then_stays_put Installs files and managed entries, preserves the default guard, and avoids unchanged rewrites Component keep Exercises the real reconciliation flow rather than only generated dictionaries.
tests/test_claude_header_mod.py::test_admin_prepend_list_is_kept_and_revert_restores_it Preserves an existing admin list and restores it on unchanged-state revert Component keep Distinct ownership coverage; extend it with the admin-edit-after-configure regression described above.
tests/test_claude_header_mod.py::test_mod_entries_never_count_as_conflicts Plugin registration does not trigger managed-setting conflicts Unit keep Directly protects the new conflict-filtering contract.
tests/test_claude_header_mod.py::test_mod_files_are_on_the_sudo_allowlist Generated files match the declared installation targets Unit consolidate Move the file-set equality assertion into the existing managed-path coverage; the subset assertion shares its source constants with the allowlist.
tests/test_claude_header_mod.py::test_unreadable_mod_folder_reads_as_inactive Permission errors produce an inactive result rather than crashing launch Unit keep Covers a material filesystem failure path.
tests/native_claude/header_mod_tests/register.test.ts — session start adds the launch headers after the managed ones Session-start merging preserves managed headers and appends launch headers Native mod harness keep Meaningful assertions on the resulting environment value.
tests/native_claude/header_mod_tests/register.test.ts — a header already sent keeps its value, and reapplying changes nothing Case-insensitive header precedence and idempotence Unit within native harness keep Protects the requirement that launch headers cannot replace existing values.
tests/native_claude/header_mod_tests/register.test.ts — without launch headers the variable is left alone Missing or blank launch headers are a no-op Native mod harness keep Protects ordinary launches without session headers.
tests/native_claude/test_mod.py::test_header_mod_validation_and_merge Claude accepts the plugin, detects its environment-write capability, and runs its native cases Native binary integration keep Validates the actual loader boundary; it does not establish organization-tier authorization or real guarded request behavior. Existing tests and fixtures in this file are otherwise unchanged.
tests/test_managed_files.py::TestManagedWorkerShell::test_real_shell_creates_missing_file[0] Writes a new file when its parent exists, preserving expected file mode and ownership handling Real shell integration keep Replaces the former [True] case without losing its distinct behavior.
tests/test_managed_files.py::TestManagedWorkerShell::test_real_shell_creates_missing_file[1] Creates one missing directory with mode 0755 under caller umask 027 Real shell integration keep Replaces the former [False] case and adds the permissions regression trigger.
tests/test_managed_files.py::TestManagedWorkerShell::test_real_shell_creates_missing_file[2] Creates two missing directory levels with readable/traversable permissions Real shell integration keep Covers the newly nested installation paths through the actual shell worker.
tests/conftest.py::_isolate_ucode_state Disables the real header-mod installation root during unrelated tests Fixture keep Prevents tests from writing beside the machine’s managed settings.
tests/README.md — header-mod coverage paragraph Documents automated coverage and the stated manual native check Test documentation keep Makes the boundary between component assertions and manual verification explicit.

Existing reconciliation and shell-allowlist parity tests provide useful supporting coverage. They do not cover the demonstrated guard-loss transition, per-request restoration after a settings reset, or the new warning/install-failure branches. The component and native-harness additions identified above are the smallest suitable extensions; another broad E2E suite is not needed for those assertions.

For manual verification, use a disposable installation: configure with no prependPlugins, append an admin plugin while retaining the generated entries, then run ug revert. Confirm the remaining explicit list still includes sec-default@builtin.

Scope

All changed files support forwarding session headers to Claude or installing, documenting, and validating that behavior. The nested-directory permission change is necessary for the installed mod to be readable. No clearly unrelated changes were found. Windows support and removing inactive mod files on revert are explicitly excluded from the stated outcome.


Automated review by Polly · workflow run

@andy-xu-db
andy-xu-db force-pushed the andy/claude-header-mod branch from c27fdb3 to ad66f00 Compare October 23, 2026 20:21
Base automatically changed from andy/ug-headers to main October 23, 2026 21:55
@andy-xu-db
andy-xu-db force-pushed the andy/claude-header-mod branch from ad66f00 to 0d1b171 Compare October 23, 2026 22:09
@andy-xu-db
andy-xu-db enabled auto-merge October 23, 2026 22:10
yaoharry pushed a commit to yaoharry/unity-gateway that referenced this pull request Oct 24, 2026
…icks#1248)

## 🥞 Stacked PR
Use this
[link](https://github.com/databricks/unity-gateway/pull/1248/files) to
review incremental changes.
-
[**andy/ug-headers**](databricks#1248)
[[Files
changed](https://github.com/databricks/unity-gateway/pull/1248/files)] ←
_this PR_
-
[andy/claude-header-mod](databricks#1240)
[[Files
changed](https://github.com/databricks/unity-gateway/pull/1240/files/68efcd391568224674249a3f973c59fa5db5be80..7c47188093226a1654efc3a836cdd18c7f972784)]

---------

## Summary

`ug --header "Name: value"` (repeatable) or `UG_HEADERS` (one `Name:
value` per line) adds headers to **one launch's** Databricks requests,
for example a LiteSwap traffic ID:

```bash
ug --header "x-databricks-traffic-id: testenv://liteswap/<pod>" codex
```

- **Codex:** the headers are added to the launch's `-c` provider
headers, for both plain and smart-routed launches. They're never written
to `~/.codex/ucode.config.toml`, so concurrent launches don't share
them. A header name that ug or the admin already sends keeps its value.
Codex merges config layers leaf by leaf, so a new name from `-c`
survives `/etc/codex/managed_config.toml`, and a name the managed file
already sets wins.
- **ug's own requests:** workspace API calls (`databricks.py`), the MCP
proxy, web search, smart-routing route selection, and the MCP OAuth
probe. The MCP proxy and web search run as children of the agent and
inherit `UG_HEADERS`. A header name ug already sets, such as
`Authorization` or `Content-Type`, keeps its value in any case: each
request lists `UG_HEADERS` first and ug's own headers after, and urllib
keeps the last value for a name.
- **Validation:** the flag and the env var go through one parser
(`ug_headers.py`). A valid line has an HTTP token name and a value with
no control characters, in Latin-1. A later line replaces an earlier one
with the same name in any case.
- A bad `--header` fails the command. The error names only the flag's
position, never the value.
- A bad inherited `UG_HEADERS` line is skipped, so it never blocks a
command, `ug revert` included.

Claude Code support is in the stacked PR databricks#1240. Claude takes
`ANTHROPIC_CUSTOM_HEADERS` whole from managed settings, so it needs a
mod.

## Testing

- `test_ug_headers.py`: the parser, plus `--header` appending to
`UG_HEADERS` and rejecting a malformed line.
- `test_agent_codex.py`: a plain launch sends the header in `-c` and
leaves the profile file unchanged, and a smart-routed launch adds new
names while keeping existing ones.
- `test_databricks.py`: ug's GET and POST helpers carry the headers.
- Full unit suite: 5330 passed. `test_e2e_user_agent` is excluded
because it fails on this dev box, where managed settings override its
capture server's URL. Ruff and ty are clean on `src/`.
- The outdated-Codex `--profile` path (before 0.134, below ug's 0.145
minimum) doesn't get the headers.
- Not checked: a real `ug codex` launch against the gateway, and Windows
Codex.

This pull request and its description were written by Isaac.

---------

Co-authored-by: Isaac <no-reply@databricks.com>
@andy-xu-db
andy-xu-db disabled auto-merge October 24, 2026 02:18
andy-xu-db and others added 5 commits October 24, 2026 02:19
Claude takes ANTHROPIC_CUSTOM_HEADERS whole from managed settings, so a
header from `ug --header` / UG_HEADERS in ug's own settings file or in the
environment never reached the gateway, and writing it into the machine-wide
file would leak it into every session.

`ug configure` (and an interactive launch) now installs a small Claude mod in
a root-owned `ucode-plugins` folder beside the managed settings, on Linux and
macOS, through the existing sudo worker; its four files join the allowlist.
The managed file enables it as the organization's mod, ahead of the built-in
guard, the one tier allowed to change a variable managed settings set. Before
each request the mod adds the session's UG_HEADERS lines to
ANTHROPIC_CUSTOM_HEADERS in that Claude process only; a header already present
keeps its value. Revert removes the managed entries; a launch warns when the
mod isn't active.

The sudo script now creates each missing parent folder root-owned and 0755:
`mkdir -p` left the folders above the innermost one with the caller's umask,
so under 027 neither ug nor Claude could read the mod.

Co-authored-by: Isaac <no-reply@databricks.com>
The sudo script created the mod's parent folders with the caller's umask
(027 left them unreadable to ug and Claude). Setting `umask 022` in the
root script fixes that for every folder `mkdir -p` makes, so the per-folder
creation helper isn't needed.

Co-authored-by: Isaac <no-reply@databricks.com>
Address review feedback on the Claude header mod:

- Install the mod's files before the managed settings name them, and add
  the entries only when every file is in place (taking ug's entries back
  out otherwise), so a failed install never enables a missing plugin for
  every user on the host. The install now shows ug's sudo notice itself.
- `ug revert` removes the mod's root-owned files and the folders they leave
  empty. When an admin has extended `prependPlugins` since configure,
  revert keeps `sec-default@builtin`, since a non-empty list without it
  turns the built-in guard off.
- `header_mod_active` checks every mod file and all three managed entries.
- The mod skips malformed and repeated UG_HEADERS lines and reads CRLF
  header strings, for launches that don't go through ug.

Co-authored-by: Isaac <no-reply@databricks.com>
Enable the mod only when this run installed its files, leaving the managed
file as is otherwise, instead of taking ug's entries back out; a run that
can't write as root no longer needs to check the files. Remove the mod's
folder on revert with one `rm -rf` of ug's fixed `ucode-plugins` path.
`header_mod_active` checks every file and `enabledPlugins`.

Co-authored-by: Isaac <no-reply@databricks.com>
A plain launch re-added the mod's managed-settings entries whenever another
writer of the file dropped them, so every session rewrote it through sudo
(CUJ 8). `ug configure` and launches with UG_HEADERS still install it.
Also stub refresh_managed_config in the header mod tests, which reached the
databricks CLI on CI.

Co-authored-by: Isaac <no-reply@databricks.com>
@andy-xu-db
andy-xu-db force-pushed the andy/claude-header-mod branch from 0d1b171 to fb7ad1a Compare October 24, 2026 02:30
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