You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
That's global — every Claude Code session in every workspace, including the Manager and any per-ticket workspace, inherits the same gateway and the same API key. That's wrong as soon as we want to:
Route different workspaces through different gateways (e.g. a non-Radius workspace shouldn't hit our Corveil gateway at all).
Use a different API key per workspace (an external client's gateway with their key, for instance).
Have any workspace at all that uses the vanilla Anthropic API (right now, every claude launch on this machine is gated through Corveil).
The fix is to move this off ~/.zshrc and into the per-workspace config that crow already manages.
Proposed shape
Extend the workspace block in {devRoot}/.claude/config.json with an optional gateway field:
When a workspace has a gateway block, every claude launch that happens "in that workspace" inherits ANTHROPIC_BASE_URL and ANTHROPIC_CUSTOM_HEADERS derived from it. When it doesn't, the env vars are explicitly unset before launch so a global ~/.zshrc setting (or a sibling workspace's gateway) doesn't bleed in.
Where the injection points live
crow-workspace setup.sh launch path — skills/crow-workspace/setup.sh:560. The claude invocation goes through crow send. Today it's:
…where $WS_* is resolved from the workspace's gateway block before the crow send call. Empty/unset workspace → use unset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS && so the inherited shell env doesn't leak through.
Manager session launch in the Crow app — Sources/Crow/App/SessionService.swift:588 (the same site that wires --permission-mode auto per managerAutoPermissionMode). The Manager session sits at devRoot and isn't bound to a single workspace, so this needs a design decision (see Open questions below).
Job-launched sessions — once crow#390 (jobs auto-permission mode) lands, jobs run a claude launch too. They should pick up the gateway of the job's workspace field. Same plumbing as Native macOS IDE with Corveil branding and GitHub integration #1 if the job ends up using setup.sh shape, or the Manager wiring if it's app-side.
Secret handling (don't punt on this)
ANTHROPIC_CUSTOM_HEADERS contains a bearer token. Plaintext in config.json is wrong — that file is already tracked in the project's .claude/ and is otherwise reviewable. Options:
op:// reference (matching the existing 1Password pattern that setup.sh and the codename-spotlight tfvars already use). The app resolves it at session-launch time via the op CLI, never persists the resolved value to disk.
macOS Keychain entry keyed by workspace id. The app reads it via the Security framework at launch; never stored in config.json.
Worktree-local .claude/settings.local.json (already ignored by per-worktree git exclude per setup.sh's attributionTrailers plumbing). Less ideal — the secret still lands on disk in plaintext, just in a gitignored location.
Recommend op:// references as the v1 default since the rest of the stack already speaks that, plus a macOS Keychain fallback for non-1Password users. Allow a plain string in customHeaders.value for convenience in dev, but warn loudly in the Settings UI that it's stored plaintext.
Open design questions
What does the Manager session use? The Manager isn't scoped to one workspace — it sits at devRoot and switches between them. Options:
(a) Manager always uses the vanilla Anthropic API (no gateway). Simple. Then per-workspace gateway only applies to sessions launched into a workspace (Crow sessions + Job sessions).
(b) Manager picks up the gateway of the current workspace in the sidebar selection. Cute, but switching workspaces would require restarting the Manager process to re-read env vars — claude reads ANTHROPIC_BASE_URL once at startup.
(c) Manager has its own global gateway in a top-level managerGateway field on AppConfig. Cleanest, lets you e.g. run the Manager against Corveil but route per-task sessions through whatever the workspace says.
Leaning toward (c) — Manager has its own gateway, per-workspace gateway applies to non-Manager sessions only.
Re-launch story. After setup.sh exits, the user often Ctrl-C and re-runs claude in the same terminal. The env vars set on the original launch line are gone. Either:
Write the env-var block to .claude/settings.local.json per-worktree and have Claude Code's env setting key pick it up (verify the current Claude Code settings schema supports this).
Drop an .envrc in the worktree (works if the user has direnv).
Have crow new-terminal plumb env into the spawned shell's env, not just the launched command, so re-running claude inherits.
Probably the third option — the crow tmux backend already manages the shell, and setting env at the tmux new-window layer would be inherited by every command in that terminal.
Per-workspace provider field already mirrors this pattern.provider: "github" vs provider: "gitlab" is per-workspace; a per-workspace gateway is the same shape. Use the same Settings UI section ("Per-workspace") and reject configs with a gateway.baseURL but no customHeaders (or vice versa) at parse time.
Acceptance
AppConfig.Workspace gains an optional gateway: { baseURL: String, customHeaders: [String:String] } block in Packages/CrowCore/Sources/CrowCore/Models/AppConfig.swift, with persistence + migration tests in ConfigStoreTests (default-nil for existing configs).
Settings → per-workspace UI exposes Base URL + Custom Headers rows, with the secret-storage strategy of choice (op:// ref / keychain / plaintext-with-warning).
skills/crow-workspace/setup.sh resolves the workspace's gateway, prepends the env-var assignments to the claude launch line, and explicitly unsets them when the workspace has no gateway block (so ~/.zshrc exports don't leak through).
Crow Manager session launch (SessionService.swift:588) picks up its own gateway via the chosen design from Open Question 1.
Re-running claude manually in a workspace terminal inherits the same env vars (Open Question 2 design landed).
Documentation: docs/configuration.md (or wherever the workspace block is documented today) gains the gateway section with the security note about secret storage.
My ~/.zshrc exports can be deleted and the same routing keeps working for the RadiusMethod workspace.
Out of scope
Adding a UI for switching gateways mid-session (env is read at claude startup; this isn't a "live" knob).
A registry of "known gateways" (Corveil, etc.) with pre-filled headers — keep v1 generic.
Gateway health-check / fallback to the vanilla API — that's the gateway's job, not the client's.
Related
The existing customInstructions per-workspace field (config.json) is the closest precedent for this shape.
The existing op:// references in terraform/environments/spotlight-commercial/terraform.tfvars.tpl are the secret-storage pattern to reuse.
The Manager-launch wiring touched by crow#390 (jobs auto-permission mode) is the same code path that needs this.
Problem
To route Claude Code through our Corveil gateway, I currently have this in
~/.zshrc:That's global — every Claude Code session in every workspace, including the Manager and any per-ticket workspace, inherits the same gateway and the same API key. That's wrong as soon as we want to:
The fix is to move this off
~/.zshrcand into the per-workspace config thatcrowalready manages.Proposed shape
Extend the workspace block in
{devRoot}/.claude/config.jsonwith an optionalgatewayfield:{ "workspaces": [ { "id": "B2C2F9BC-…", "name": "RadiusMethod", "provider": "github", "cli": "gh", "gateway": { "baseURL": "https://corveil.io", "customHeaders": { "x-citadel-api-key": "Bearer sk-citadel-…" // alternative: { "value": "...", "secret_ref": "op://Spotlight Prod/Citadel/api_key" } } } }, { "id": "F82E0C3A-…", "name": "Personal", "provider": "github", "cli": "gh" // no gateway block → use vanilla Anthropic API (env unset) } ] }When a workspace has a
gatewayblock, everyclaudelaunch that happens "in that workspace" inheritsANTHROPIC_BASE_URLandANTHROPIC_CUSTOM_HEADERSderived from it. When it doesn't, the env vars are explicitly unset before launch so a global~/.zshrcsetting (or a sibling workspace's gateway) doesn't bleed in.Where the injection points live
crow-workspacesetup.sh launch path —skills/crow-workspace/setup.sh:560. Theclaudeinvocation goes throughcrow send. Today it's:It needs to become something like:
…where
$WS_*is resolved from the workspace'sgatewayblock before thecrow sendcall. Empty/unset workspace → useunset ANTHROPIC_BASE_URL ANTHROPIC_CUSTOM_HEADERS &&so the inherited shell env doesn't leak through.Manager session launch in the Crow app —
Sources/Crow/App/SessionService.swift:588(the same site that wires--permission-mode autopermanagerAutoPermissionMode). The Manager session sits atdevRootand isn't bound to a single workspace, so this needs a design decision (see Open questions below).Job-launched sessions — once crow#390 (jobs auto-permission mode) lands, jobs run a
claudelaunch too. They should pick up the gateway of the job'sworkspacefield. Same plumbing as Native macOS IDE with Corveil branding and GitHub integration #1 if the job ends up usingsetup.shshape, or the Manager wiring if it's app-side.Secret handling (don't punt on this)
ANTHROPIC_CUSTOM_HEADERScontains a bearer token. Plaintext inconfig.jsonis wrong — that file is already tracked in the project's.claude/and is otherwise reviewable. Options:op://reference (matching the existing 1Password pattern thatsetup.shand the codename-spotlight tfvars already use). The app resolves it at session-launch time via theopCLI, never persists the resolved value to disk..claude/settings.local.json(already ignored by per-worktree git exclude persetup.sh'sattributionTrailersplumbing). Less ideal — the secret still lands on disk in plaintext, just in a gitignored location.Recommend
op://references as the v1 default since the rest of the stack already speaks that, plus a macOS Keychain fallback for non-1Password users. Allow a plain string incustomHeaders.valuefor convenience in dev, but warn loudly in the Settings UI that it's stored plaintext.Open design questions
What does the Manager session use? The Manager isn't scoped to one workspace — it sits at
devRootand switches between them. Options:claudereadsANTHROPIC_BASE_URLonce at startup.managerGatewayfield on AppConfig. Cleanest, lets you e.g. run the Manager against Corveil but route per-task sessions through whatever the workspace says.Leaning toward (c) — Manager has its own gateway, per-workspace gateway applies to non-Manager sessions only.
Re-launch story. After
setup.shexits, the user oftenCtrl-Cand re-runsclaudein the same terminal. The env vars set on the original launch line are gone. Either:.claude/settings.local.jsonper-worktree and have Claude Code'senvsetting key pick it up (verify the current Claude Code settings schema supports this)..envrcin the worktree (works if the user has direnv).crow new-terminalplumb env into the spawned shell's env, not just the launched command, so re-runningclaudeinherits.Probably the third option — the crow tmux backend already manages the shell, and setting env at the
tmux new-windowlayer would be inherited by every command in that terminal.Per-workspace
providerfield already mirrors this pattern.provider: "github"vsprovider: "gitlab"is per-workspace; a per-workspacegatewayis the same shape. Use the same Settings UI section ("Per-workspace") and reject configs with agateway.baseURLbut nocustomHeaders(or vice versa) at parse time.Acceptance
AppConfig.Workspacegains an optionalgateway: { baseURL: String, customHeaders: [String:String] }block inPackages/CrowCore/Sources/CrowCore/Models/AppConfig.swift, with persistence + migration tests inConfigStoreTests(default-nil for existing configs).Base URL+Custom Headersrows, with the secret-storage strategy of choice (op://ref / keychain / plaintext-with-warning).skills/crow-workspace/setup.shresolves the workspace's gateway, prepends the env-var assignments to theclaudelaunch line, and explicitly unsets them when the workspace has no gateway block (so~/.zshrcexports don't leak through).SessionService.swift:588) picks up its own gateway via the chosen design from Open Question 1.claudemanually in a workspace terminal inherits the same env vars (Open Question 2 design landed).docs/configuration.md(or wherever the workspace block is documented today) gains the gateway section with the security note about secret storage.~/.zshrcexports can be deleted and the same routing keeps working for the RadiusMethod workspace.Out of scope
claudestartup; this isn't a "live" knob).Related
customInstructionsper-workspace field (config.json) is the closest precedent for this shape.op://references interraform/environments/spotlight-commercial/terraform.tfvars.tplare the secret-storage pattern to reuse.