feat(runtime): per-profile environment overrides for Claude adapter#127
Conversation
Add a `options.environment` field on Claude runtime profiles that injects profile-scoped env vars into both transports: - SDK: through `parseExecutionOptions().environment` (merged after legacy `execution.hooks.environment`, before per-call `execution.environment`). - CLI: through `buildCuratedEnv()` in `cli.ts` (same merge order so per-call overrides keep precedence). Primary use case is multi-account Claude Code on a single host: pin `CLAUDE_CONFIG_DIR` per profile so different Handoff projects can run under different `~/.claude/` home directories without mutating the host process environment or rebuilding the container. Behavior: - Values must be strings; non-string entries are silently dropped. - Precedence: `execution.hooks.environment` < `options.environment` < `execution.environment`. - Fully additive — existing profiles without `options.environment` behave identically. Includes three CLI adapter tests (basic injection, per-call override, non-string filtering) and a new "Multi-account example" section in docs/providers.md.
Code reviewThis PR adds profile-scoped environment injection for Claude runtime profiles. The implementation is small and additive, CI is green, and I did not find a blocker. I also checked it against PR #126: there is no direct file overlap in the current PR diffs. The only semantic overlap is that #126 pins Must fixNone. Should fix
Nits
Context gates
Verdict: COMMENT - no blockers; please add the SDK regression coverage before relying on the documented SDK behavior. |
Address PR review feedback:
- Add SDK regression test in claudeAdapter.test.ts asserting that
profile.options.environment lands in `query()`'s `options.env` and
that the documented precedence holds: hooks < options < execution.
Mirrors the existing CLI coverage so the SDK contract is protected
independently.
- Reject arrays in `resolveProfileEnvironment` with an explicit
plain-object guard. Without this, `environment: ["x"]` would have
leaked through as `{ "0": "x" }` because `typeof [] === "object"`.
Adds a paired SDK test asserting array entries are dropped.
|
Thanks for the thorough review @lee-to. Both points addressed in 6674787 (force-pushed onto the same branch). Should fix #1 — SDK regression coverage (claudeAdapter.test.ts):
Nit — array guard in Stats: 788/788 tests passing (+2 new SDK tests on top of the original 786), lint and prettier green, full PR diff is now 197 lines / 4 files / still 1 concern. |
Code review follow-upResponse pass on the previous review comment: both items are addressed in Addressed
New findingsNone. Verification
Context gates
Verdict: LGTM on code. I would merge after the remaining GitHub |
Summary
options.environmentfield on Claude runtime profiles that injects profile-scoped env vars into the spawned Claude subprocess (CLI transport) and the Claude SDK execution options (SDK transport).CLAUDE_CONFIG_DIRper profile, so different Handoff projects can use different~/.claude/home directories without mutating the host process environment, restarting Handoff, or rebuilding containers.options.environmentbehave exactly as before.Use case
Today, a single Handoff host shares one Claude identity across every project: subagents inherit credentials, plugins, MCP servers, memory, and chat history from whatever
~/.claude/the Handoff process started under. Teams that want to keep personal/work or per-client Claude accounts isolated have to either (a) run separate Handoff instances on different ports, or (b) rebuild the container with a different baked-in env — both heavy.With this PR a user can keep one Handoff instance, define two Claude CLI profiles:
{ "name": "Claude CLI (personal)", "runtimeId": "claude", "providerId": "anthropic", "transport": "cli", "options": { "environment": { "CLAUDE_CONFIG_DIR": "/Users/me/.claude-personal" } } }{ "name": "Claude CLI (work)", "runtimeId": "claude", "providerId": "anthropic", "transport": "cli", "options": { "environment": { "CLAUDE_CONFIG_DIR": "/Users/me/.claude-work" } } }…assign each as the default task runtime for a different project, and subagents spawned for that project read credentials/plugins/history from the matching
~/.claude-*/directory. No host env mutation, no per-project Handoff process.Changes
packages/runtime/src/adapters/claude/options.tsresolveProfileEnvironment(input)readsprofile.options.environmentand returns aRecord<string, string>(non-string values dropped).parseExecutionOptions()merges the helper output intoexecution.environmentbetween the legacyexecution.hooks.environmentsource and the per-callexecution.environmentoverride.packages/runtime/src/adapters/claude/cli.tsresolveProfileEnvironmentand merges it into theexecutionEnvargument passed tobuildCuratedEnv()so the spawnedclaudesubprocess receives the profile env vars. Same precedence as the SDK path.packages/runtime/src/__tests__/claudeCli.test.tsspawn:spawnOptions.envexecution.environmentoverrides profile valuesdocs/providers.mdCLAUDE_CONFIG_DIRvalues, with a note thatCLAUDE_CONFIG_DIRonly meaningfully affects subprocess transports.Behavior contract
toStringRecordhelper used elsewhere in the file).execution.hooks.environment→options.environment→execution.environment.cli.ts:ALLOWED_ENV_PREFIXESalready letsCLAUDE_*through fromprocess.env, but that path is host-wide —options.environmentis the per-profile equivalent.runtimeOptionsSchema = z.record(z.string(), z.unknown())already accepts arbitrary keys underoptions.optionsJsonis persisted as-is in SQLite, so no migration is needed.Backwards compatibility
100% additive. Profiles without
options.environmentproduce identicalexecution.environmentand identical spawned-subprocess env. All existing tests (786) pass unchanged.AI Model
Test Plan
npx turbo test --filter=@aif/runtime— 786/786, including 3 new CLI adapter tests)npx turbo lint --filter=@aif/runtime)npx prettier --checkon touched files)npx turbo build— 7/7)nodescript that importsdist/adapters/claude/options.jsand asserts:resolveProfileEnvironmentfilters non-string values;parseExecutionOptionsmerges profile env intoexecution.environment; per-callexecution.environmentwins over profile;options.environmentwins over legacyexecution.hooks.environment)docs/providers.mdClaude (CLI) section)