Per-workspace AI gateway config (ANTHROPIC_BASE_URL / ANTHROPIC_CUSTOM_HEADERS)#403
Per-workspace AI gateway config (ANTHROPIC_BASE_URL / ANTHROPIC_CUSTOM_HEADERS)#403dhilgaertner wants to merge 7 commits into
Conversation
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Solid, well-tested implementation overall — model / resolver / launch-prefix / merge writer / setup.sh helpers are cleanly separated, parse-time validation rejects half-filled gateways, and op:// resolution failures fail loud (gateway rejects) rather than silently falling back to vanilla. Round-trip and bash tests are thorough.
Two Yellow findings around the no-leak guarantee and at-rest secret protection that I'd like addressed in this round.
Yellow Findings
Y1 — Multi-header launch line leaves a ~/.zshrc leak path open. ClaudeLaunchArgs.gatewayEnvPrefix (Packages/CrowCore/Sources/CrowCore/ClaudeLaunchArgs.swift:59-68) and gateway_launch_prefix in setup.sh:148-157 both intentionally omit ANTHROPIC_CUSTOM_HEADERS from the launch line when there is more than one header (to avoid embedding a literal newline that would submit the command early). In that branch the prefix becomes just ANTHROPIC_BASE_URL='…' claude …, which is a command-prefix assignment — it sets ANTHROPIC_BASE_URL for the launched process but does not clear an ANTHROPIC_CUSTOM_HEADERS that the shell inherited from ~/.zshrc. Claude Code then sees the gateway's baseURL paired with the user's stale global headers (or no headers, depending on precedence with settings.local.json env). The PR description's "load-bearing no-leak guard" claim doesn't hold in the multi-header case.
Why: the PR explicitly motivates the launch-line prefix as "the load-bearing no-leak guard"; this is the one branch where it isn't.
How to apply: in the multi-header branch, prepend unset ANTHROPIC_CUSTOM_HEADERS && (or wrap the whole launch in env -u ANTHROPIC_CUSTOM_HEADERS …) before the ANTHROPIC_BASE_URL=… assignment, so the inherited shell var is removed before claude starts. Cover it with a test that asserts both unset ANTHROPIC_CUSTOM_HEADERS and ANTHROPIC_BASE_URL=… appear in the multi-header prefix.
Y2 — Resolved secrets persisted to settings.local.json with default file permissions. The PR description / Settings UI describe op:// as the "nothing-secret-at-rest" mode, but the resolved secret value is written into .claude/settings.local.json's env block on every launch:
HookConfigGenerator.writeGatewayEnv(Sources/Crow/App/HookConfigGenerator.swift:109-147) writes viadata.write(to:)without setting permissions.setup.sh:write_settings_local(skills/crow-workspace/setup.sh:431-502) writes viaprintf > "$settings_path"withoutchmod 600.
ConfigStore.saveConfig already sets 0600 on config.json for the same reason. settings.local.json now carries the same sensitivity (the post-op read plaintext bearer token) but inherits the umask default (typically 0644).
Why: the design intent is that op:// keeps secrets off disk; in practice the resolved value lands on disk in a file with weaker protection than config.json.
How to apply: when the merged settings dict contains env.ANTHROPIC_CUSTOM_HEADERS (or any non-empty env block), chmod 0600 the file after writing — both in HookConfigGenerator.writeGatewayEnv (use FileManager.default.setAttributes([.posixPermissions: 0o600], ofItemAtPath: settingsPath)) and in setup.sh:write_settings_local (chmod 600 "$settings_path"). Additionally, soften the docs / Settings UI copy: op:// means "nothing secret in config.json," not "nothing secret on disk" — the resolved value is cached in settings.local.json for the worktree's lifetime.
Green / Nits (consider, not blocking)
- G1 — The "precedence open item" in the PR description is real. With
ANTHROPIC_BASE_URLin~/.zshrcand a different one insettings.local.jsonenv, the test suite doesn't pin down which wins. Worth adding a Claude-Code-side smoke test (or at minimum a doc note indocs/configuration.md#ai-gateway) once you verify it manually, so the next person doesn't have to re-derive it. - G2 —
is_remote_control_enabled/is_attribution_trailers_enabledusetr -d '\n' | grep -qEinstead ofjq(skills/crow-workspace/setup.sh:76-95). Pre-existing, not introduced here, but worth a follow-up to converge on the jq-based path used byresolve_gateway_env. - G3 —
op read15s timeout (Packages/CrowCore/Sources/CrowCore/GatewayResolver.swift:94-104) is reasonable for biometric prompts; could be made configurable later if Touch ID prompts feel sluggish.
Security Review
Strengths:
posix_quotein setup.sh correctly handles single-quote escaping; matchesClaudeLaunchArgs.shellQuote.- Resolved secret values are never logged (NSLog messages drop the header value, only naming the header key).
- Failed
op readdrops the header rather than falling back silently — gateway 401 surfaces the misconfig. gateway_launch_prefixuses single-quoted assignments, so a hostilebaseURLvalue can't break out of quoting.- Parse-time rejection of half-filled gateways prevents the "baseURL without auth" footgun.
op readinvocation uses$valuequoted, so anop://…string with shell metachars can't inject.
Concerns:
- Y1 and Y2 above.
Code Quality
- Tests cover the model, persistence, resolver (incl. failure paths), the launch prefix builder, the bash helpers (15/15 passing locally), and UI form validation. Good.
- Backward compat is correct — every new field uses
decodeIfPresentwith a default; legacy configs decode unchanged. WorkspaceGateway.parseHeaderLines/headerLinesform a clean round-trip for the editor.GatewayResolveris dependency-injected (resolveSecret:) so the resolver itself is testable withoutop.- Single source for the workspace-name-from-worktree-path math (
SessionService.workspaceName), good for testability. - Settings UI surfaces validation inline and blocks save on a half-filled block — matches the parse-time invariant.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Request Changes — driven by 0 Red, 2 Yellow, 3 Green findings. The two Yellows are scoped and should be quick to land in the same round trip.
Two Yellow findings from the PR #403 review (CROW-402): Y1 — Multi-header launch line no longer leaves a ~/.zshrc leak. When a gateway has more than one header the value can't go on the launch line (embedded newline), so it was carried only by settings.local.json — but the prefix was a bare `ANTHROPIC_BASE_URL=… claude`, which doesn't clear an inherited ANTHROPIC_CUSTOM_HEADERS. The multi-header branch now emits `unset ANTHROPIC_CUSTOM_HEADERS && ANTHROPIC_BASE_URL=… ` in both ClaudeLaunchArgs.gatewayEnvPrefix and setup.sh's gateway_launch_prefix, so the gateway's baseURL is never paired with stale global headers. Tests updated. Y2 — Resolved secrets in settings.local.json are now written 0600. The env block caches a post-`op read` bearer token, so HookConfigGenerator.writeGatewayEnv and setup.sh's write_settings_local now chmod the file owner-only, matching ConfigStore's 0600 on config.json. Added a permission assertion to the bash test. Also softened the docs / Settings UI copy: `op://` means "kept out of config.json," not "no secret on disk" — the resolved value is cached (0600) in the worktree's settings.local.json. Added a precedence note to configuration.md (G1): the launch-line assignment is what reliably overrides a shell export; the end state is to delete the global ~/.zshrc exports. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
|
Thanks for the review — both Yellows addressed in Y1 — multi-header leak path (fixed). The multi-header branch now
So the gateway's Y2 — 0600 on settings.local.json (fixed). Both writers now restrict the file to owner-only after writing, matching
Added a G1 — precedence (doc note added). Added a precedence note to G2 / G3 — acknowledged, deferred. G2 (converging All suites green: CrowCore 212, root 145, |
dgershman
left a comment
There was a problem hiding this comment.
Code & Security Review
Re-review after the fix commit (0f0a7e3). Both Yellow findings from the prior review and G1 are closed cleanly; no new Red/Yellow findings emerged.
Verification of prior findings
- Y1 — multi-header
~/.zshrcleak path is closed.ClaudeLaunchArgs.gatewayEnvPrefix(Packages/CrowCore/Sources/CrowCore/ClaudeLaunchArgs.swift:60-69) andgateway_launch_prefix(skills/crow-workspace/setup.sh:150-161) both now emitunset ANTHROPIC_CUSTOM_HEADERS && ANTHROPIC_BASE_URL='…'in the multi-header branch.gatewayEnvPrefixUnsetsInheritedHeadersForMultiLine(GatewayResolverTests.swift:77-92) and the bash test ("multi-header prefix unsets inherited headers", "multi-header prefix omits headers assignment", "launch prefix has no embedded newline") pin the contract. - Y2 — resolved secrets at-rest now owner-only.
HookConfigGenerator.writeGatewayEnv(Sources/Crow/App/HookConfigGenerator.swift:143-146) callsFileManager.setAttributes([.posixPermissions: 0o600], …)after every write.setup.sh:write_settings_local(skills/crow-workspace/setup.sh:487-489)chmod 600s the file. The bash test asserts the mode is600. - G1 — precedence note is in docs/configuration.md (note at lines 100-102): the launch-line assignment is the reliable override, and the end state is to delete the global
~/.zshrcexports. Docs/UI copy also softened —op://is now described as "kept out of config.json," not "no secret on disk."
Tests
swift test --filter GatewayResolverTests— 8/8 green locally.bash skills/crow-workspace/setup_gateway_test.sh— 17/17 green locally (the new 0600 assertion + the two new multi-header assertions land).
Security Review
Strengths (re-confirmed):
posix_quote/shellQuotePOSIX-escape correctly; hostilebaseURLvalues can't break quoting.- Resolved secret values are still never logged (NSLog drops the value, only naming the header key).
- Failed
op readdrops the header rather than falling back silently — the gateway returns a loud 401. - Parse-time rejection of half-filled gateways stops "baseURL without auth" at decode.
- Both
writeGatewayEnvandwrite_settings_localnow matchConfigStore.saveConfig's 0600 stance on the at-rest secret.
Concerns: None blocking.
Code Quality
- Behavior change is minimal and well-targeted; doc comments on both prefix builders accurately describe all three branches (nil / single-header / multi-header).
try?onsetAttributesinwriteGatewayEnvmirrors existing best-effort logging patterns elsewhere.- Backward compat unchanged — every new field still uses
decodeIfPresentwith a default.
Green / Nits (consider, not blocking)
- G2 (carried over) —
is_remote_control_enabled/is_attribution_trailers_enabledstill usetr | grepinstead ofjq. Pre-existing; worth a follow-up to converge on the jq-based path used byresolve_gateway_env. - G3 (carried over) —
op read15s timeout is reasonable; could be made configurable later if Touch ID prompts ever feel sluggish. - G4 — brief perm race on
settings.local.json. InlaunchClaude,writeHookConfigruns beforewriteGatewayEnv; its atomic write recreates the file with default umask (~0644) beforewriteGatewayEnvre-applies 0600. The window is microseconds on a single-user macOS app, end state is 0600, and no exploit is plausible — butwriteHookConfigcould also set 0600 unconditionally to close it. - G5 —
AutomationSettingsView.commitManagerGatewayleaves the previous valid gateway in place when the form goes half-filled. The inline red error tells the user, and the form has no Save button to commit a bad state. UX nit, not security.
Summary Table
| Color | Meaning | Verdict effect |
|---|---|---|
| Red | Must fix | Request changes |
| Yellow | Should fix | Request changes |
| Green | Consider | Approve allowed |
Recommendation: Approve — driven by 0 Red, 0 Yellow, 5 Green findings. The two prior Yellows (Y1 launch-line leak, Y2 0600 at-rest) and G1 (precedence note) all landed cleanly with test coverage.
Two Yellow findings from the PR #403 review (CROW-402): Y1 — Multi-header launch line no longer leaves a ~/.zshrc leak. When a gateway has more than one header the value can't go on the launch line (embedded newline), so it was carried only by settings.local.json — but the prefix was a bare `ANTHROPIC_BASE_URL=… claude`, which doesn't clear an inherited ANTHROPIC_CUSTOM_HEADERS. The multi-header branch now emits `unset ANTHROPIC_CUSTOM_HEADERS && ANTHROPIC_BASE_URL=… ` in both ClaudeLaunchArgs.gatewayEnvPrefix and setup.sh's gateway_launch_prefix, so the gateway's baseURL is never paired with stale global headers. Tests updated. Y2 — Resolved secrets in settings.local.json are now written 0600. The env block caches a post-`op read` bearer token, so HookConfigGenerator.writeGatewayEnv and setup.sh's write_settings_local now chmod the file owner-only, matching ConfigStore's 0600 on config.json. Added a permission assertion to the bash test. Also softened the docs / Settings UI copy: `op://` means "kept out of config.json," not "no secret on disk" — the resolved value is cached (0600) in the worktree's settings.local.json. Added a precedence note to configuration.md (G1): the launch-line assignment is what reliably overrides a shell export; the end state is to delete the global ~/.zshrc exports. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
0f0a7e3 to
1afbb20
Compare
|
Rebased onto
Verification on the rebased branch: |
Two Yellow findings from the PR #403 review (CROW-402): Y1 — Multi-header launch line no longer leaves a ~/.zshrc leak. When a gateway has more than one header the value can't go on the launch line (embedded newline), so it was carried only by settings.local.json — but the prefix was a bare `ANTHROPIC_BASE_URL=… claude`, which doesn't clear an inherited ANTHROPIC_CUSTOM_HEADERS. The multi-header branch now emits `unset ANTHROPIC_CUSTOM_HEADERS && ANTHROPIC_BASE_URL=… ` in both ClaudeLaunchArgs.gatewayEnvPrefix and setup.sh's gateway_launch_prefix, so the gateway's baseURL is never paired with stale global headers. Tests updated. Y2 — Resolved secrets in settings.local.json are now written 0600. The env block caches a post-`op read` bearer token, so HookConfigGenerator.writeGatewayEnv and setup.sh's write_settings_local now chmod the file owner-only, matching ConfigStore's 0600 on config.json. Added a permission assertion to the bash test. Also softened the docs / Settings UI copy: `op://` means "kept out of config.json," not "no secret on disk" — the resolved value is cached (0600) in the worktree's settings.local.json. Added a precedence note to configuration.md (G1): the launch-line assignment is what reliably overrides a shell export; the end state is to delete the global ~/.zshrc exports. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
1afbb20 to
e96c6a5
Compare
Adds an optional AI gateway to the config so `claude` launches can route
through a per-workspace ANTHROPIC_BASE_URL / ANTHROPIC_CUSTOM_HEADERS instead
of a global ~/.zshrc export (CROW-402):
- WorkspaceGateway { baseURL, customHeaders }; validating decode rejects a
half-filled block (baseURL xor customHeaders) at parse time.
- WorkspaceInfo.gateway and AppConfig.managerGateway, both optional/default-nil
so existing configs decode unchanged. The Manager has its own gateway because
it sits at devRoot and isn't bound to one workspace.
- Tests: legacy decode → nil, round-trip through encode/decode and ConfigStore
disk I/O, malformed-block rejection surfaces as a load failure.
🐦⬛ Generated with Claude Code, orchestrated by Crow
Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
GatewayResolver turns a WorkspaceGateway into launch-ready ANTHROPIC_BASE_URL and ANTHROPIC_CUSTOM_HEADERS values (CROW-402): - Header values prefixed `op://` are resolved via the 1Password CLI (`op read`, bounded by a 15s timeout); any other value is used literally (plaintext). - Headers serialize to newline-separated `Name: Value`, sorted for determinism — the format Claude Code expects for ANTHROPIC_CUSTOM_HEADERS. - A failed secret resolution drops that header (logged, redacted) but keeps the baseURL, so the gateway rejects the request loudly rather than silently falling back to the vanilla Anthropic API. Resolved secrets are never logged. - `resolveSecret` is injectable, so tests cover plaintext/op/failure paths without shelling out. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
Wires the resolved gateway into the two app-side launch paths (CROW-402): - HookConfigGenerator.writeGatewayEnv merges (or clears) an `env` block in a directory's .claude/settings.local.json, so manual `claude` re-runs in the terminal inherit the gateway — not just the initial launch. - ClaudeLaunchArgs.gatewayEnvPrefix builds the launch-line prefix: command-prefix assignments when a gateway is present (overriding ~/.zshrc for that launch), or `unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS &&` when absent so a no-gateway workspace can't inherit a sibling's or ~/.zshrc's gateway. Multi-line header values are omitted from the line (settings.local.json carries them) to avoid pasting a newline that would submit the command early. - launchClaude resolves the session's workspace by the worktree's first path component under devRoot; managerCommand uses AppConfig.managerGateway. Both write the settings.local.json env block and prepend the prefix. Tests cover the prefix forms; the existing managerCommand test still passes. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
Wires the per-workspace gateway into the crow-workspace launch path (CROW-402):
- resolve_gateway_env reads the workspace's gateway from
{devRoot}/.claude/config.json via jq, resolving op:// header values through
`op read` (plaintext values pass through). Resolved values are never logged.
- write_settings_local now merges (via jq) a gateway `env` block alongside the
existing attribution trailer into settings.local.json — preserving any existing
keys — so manual `claude` re-runs in the terminal inherit the gateway. The env
block is written independently of the attributionTrailers setting.
- The launch line is prefixed with the gateway env assignments (or
`unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS &&` when no gateway), so the
initial launch overrides ~/.zshrc and a no-gateway workspace can't inherit it.
Multi-line header values are omitted from the line (carried by settings.local.json).
- main is now guarded so the helpers can be sourced by setup_gateway_test.sh,
which covers op:// resolution, the settings merge, and both prefix forms.
🐦⬛ Generated with Claude Code, orchestrated by Crow
Co-Authored-By: Claude <noreply@anthropic.com>
Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
Adds gateway configuration to Settings (CROW-402): - WorkspaceFormView gains an "AI Gateway" section (Base URL + a Name: Value headers editor) with an inline note that op:// references resolve at launch while plaintext is stored in config.json. Save is blocked on a half-filled block, matching the decoder's parse-time validation. - AutomationSettingsView gains a "Manager AI Gateway" section bound to AppConfig.managerGateway, beside the Manager auto-permission toggle, since the Manager has its own gateway. - WorkspaceGateway.parseHeaderLines / headerLines convert between the dict and the multiline editor text (shared by both views), with tests. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
Adds an "AI Gateway" section to configuration.md covering the workspace `gateway` and top-level `managerGateway` blocks (CROW-402): the op:// vs plaintext secret storage rules, the both-fields-or-neither validation, and the two-way injection (launch line + settings.local.json) that makes routing survive manual re-runs and keeps a no-gateway workspace from inheriting a global ~/.zshrc export. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
Two Yellow findings from the PR #403 review (CROW-402): Y1 — Multi-header launch line no longer leaves a ~/.zshrc leak. When a gateway has more than one header the value can't go on the launch line (embedded newline), so it was carried only by settings.local.json — but the prefix was a bare `ANTHROPIC_BASE_URL=… claude`, which doesn't clear an inherited ANTHROPIC_CUSTOM_HEADERS. The multi-header branch now emits `unset ANTHROPIC_CUSTOM_HEADERS && ANTHROPIC_BASE_URL=… ` in both ClaudeLaunchArgs.gatewayEnvPrefix and setup.sh's gateway_launch_prefix, so the gateway's baseURL is never paired with stale global headers. Tests updated. Y2 — Resolved secrets in settings.local.json are now written 0600. The env block caches a post-`op read` bearer token, so HookConfigGenerator.writeGatewayEnv and setup.sh's write_settings_local now chmod the file owner-only, matching ConfigStore's 0600 on config.json. Added a permission assertion to the bash test. Also softened the docs / Settings UI copy: `op://` means "kept out of config.json," not "no secret on disk" — the resolved value is cached (0600) in the worktree's settings.local.json. Added a precedence note to configuration.md (G1): the launch-line assignment is what reliably overrides a shell export; the end state is to delete the global ~/.zshrc exports. 🐦⬛ Generated with Claude Code, orchestrated by Crow Co-Authored-By: Claude <noreply@anthropic.com> Crow-Session: E3DFAF9C-38CB-4D95-979D-9CC48F4EC3C9
e96c6a5 to
d8bc867
Compare
Closes #402
Routes Claude Code through a per-workspace AI gateway (
ANTHROPIC_BASE_URL/ANTHROPIC_CUSTOM_HEADERS) instead of a global~/.zshrcexport, so different workspaces can use different gateways/keys — or the vanilla Anthropic API — and a non-Radius workspace never inherits the Corveil gateway.Design decisions
managerGateway. The Manager sits at devRoot and isn't bound to one workspace; a top-levelmanagerGatewaymirrors the existingmanagerAutoPermissionModepattern. Per-workspacegatewayapplies to non-Manager sessions only.settings.local.jsonenvblock (not tmux-e). setup.sh already writes.claude/settings.local.json(attribution) and the app already merges into it; Claude Code reads itsenvkey on every run, so this covers initial launch and manualclaudere-runs with no tmux plumbing and no secret inps/argv. A launch-line prefix (orunset … &&when no gateway) handles the initial-launch override and the no-leak guard. This supersedes the ticket's tmux-eoption, which would put the token in process args and lose to~/.zshrcsourcing anyway.WorkspaceGatewaydecode throws on a half-filled block (baseURL xor customHeaders); the Settings UI blocks save on the same condition so a rejectable block is never written.Secret handling
Default is
op://reference + plaintext-with-warning; Keychain deferred behind a cleanGatewayResolverseam (you're a 1Password shop, andop://already keeps nothing-secret-at-rest). To flip:op://(recommended) — header value likeop://Vault/Item/field, resolved at launch viaop read(app-side and in setup.sh). Nothing secret inconfig.json.op://value; stored inconfig.json(0600) with a red warning in Settings.oplookup drops that header (redacted log) and keeps the baseURL, so the gateway returns a loud 401 rather than silently falling back to vanilla. Resolved secrets are never logged.What changed (commit per phase)
WorkspaceGatewaymodel +WorkspaceInfo.gateway+AppConfig.managerGateway, validating decode, tests.GatewayResolver— resolveop://, serializeName: Valueheaders, tests.op read, mergeenvinto settings.local.json, launch-line prefix /unset;setup_gateway_test.sh.HookConfigGenerator.writeGatewayEnv,ClaudeLaunchArgs.gatewayEnvPrefix,launchClaude(work/review/job) + Manager wiring.docs/configuration.mdAI Gateway section.Tests
swift testgreen: CrowCore (212), CrowPersistence (28), root app target (145).bash skills/crow-workspace/setup_gateway_test.sh— 15/15 (op:// resolution via a fakeop, settings merge, both prefix forms).make buildclean.Manual verification (for the reviewer / @dustinhilgaertner to run)
https://corveil.io, headerx-citadel-api-key: op://…) in Settings → Workspaces.ANTHROPIC_BASE_URL/ANTHROPIC_CUSTOM_HEADERSexports in~/.zshrc.ANTHROPIC_BASE_URLstill in~/.zshrcand a different one in the worktree'ssettings.local.jsonenv, confirm which wins. The launch-line prefix makes the override correct regardless, but this confirms whether settingsenvalone suffices.Coordination
crow#390 (jobs auto-permission mode) is already merged/closed — this is independent, no rebase. Job sessions pick up their workspace's gateway through the same
launchClaudepath.