feat(claude): spawn hosted sessions under the bound account's CLAUDE_CONFIG_DIR (T-484)
The load-bearing piece of the multi-account epic (T-476): a bound workspace's hosted claude now spawns with CLAUDE_CONFIG_DIR set to that account's dir, so it runs under the bound account end-to-end. Every hosted session (primary / secondary / fork / teammate) inherits it — the orchestrator already routes all spawns through agentBootstrap. - New pure resolver claudeConfigDirForWorkspace(cwd, boundConfigDir, env): bound account dir > parent CLAUDE_CONFIG_DIR (respect the launcher) > null (Claude defaults to ~/.claude). The registry is injected as a plain lookup so agent_bootstrap stays Flutter-free (its tests run under `dart test`). - agentBootstrap merges CLAUDE_CONFIG_DIR BEFORE base, so an explicit SpawnSpec.env override still wins (override > binding > parent > unset); the key is omitted entirely when the resolver returns null. - Orchestrator carries an optional AccountRegistry; the claude extension builds it from ctx.settings. Null in tests → no injection (unchanged behaviour). No way to SET a binding yet (that's the CLI T-480 / settings UI T-482), so no changelog entry — the mechanism is in place, the surface lands next. Unit tests cover the resolver's four states and the envDelta precedence. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6788,3 +6788,4 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
|||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6QCYT7JBF352YVJ48', 'status', 'in_progress', 'done', NULL, '2026-06-27 13:51:01', '2026-06-27 13:51:01.108', '2026-06-27 13:51:01.108', NULL, 'd87bdd8b0216a2362c9c41d9f5fa0afa', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6QCYT7JBF352YVJ48', 'status', 'in_progress', 'done', NULL, '2026-06-27 13:51:01', '2026-06-27 13:51:01.108', '2026-06-27 13:51:01.108', NULL, 'd87bdd8b0216a2362c9c41d9f5fa0afa', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'status', 'backlog', 'in_progress', NULL, '2026-06-27 14:40:34', '2026-06-27 14:40:34.929', '2026-06-27 14:40:34.929', NULL, '72f31963c666be8d4891f4886c4ddc3f', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'status', 'backlog', 'in_progress', NULL, '2026-06-27 14:40:34', '2026-06-27 14:40:34.929', '2026-06-27 14:40:34.929', NULL, '72f31963c666be8d4891f4886c4ddc3f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'status', 'in_progress', 'done', NULL, '2026-06-27 14:42:39', '2026-06-27 14:42:39.553', '2026-06-27 14:42:39.553', NULL, 'fcd6750756465c07eb8a568ef7c26362', 2) ON CONFLICT(hash) DO NOTHING;
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49VYMPF18PYQXD9PCMHN8', 'status', 'in_progress', 'done', NULL, '2026-06-27 14:42:39', '2026-06-27 14:42:39.553', '2026-06-27 14:42:39.553', NULL, 'fcd6750756465c07eb8a568ef7c26362', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'status', 'backlog', 'done', NULL, '2026-06-27 14:59:54', '2026-06-27 14:59:54.883', '2026-06-27 14:59:54.883', NULL, '84fd321fa07b452d52be53a28e433d9a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -8580,3 +8580,42 @@ All persistence flows through SettingsStore so the existing `.clide/settings.yam
|
|||||||
- T-481 (UI) — renders registry + binding state; calls the CRUD via T-480.
|
- T-481 (UI) — renders registry + binding state; calls the CRUD via T-480.
|
||||||
- T-482 (settings UI) — schema-driven rows hit these keys.
|
- T-482 (settings UI) — schema-driven rows hit these keys.
|
||||||
', 'done', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 14:42:39.552', NULL, '74c4a4292386cf5b311a4475551a65e1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
', 'done', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 14:42:39.552', NULL, '74c4a4292386cf5b311a4475551a65e1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FFW49W3V175EM4F8ZHC6JFM4', 'task', '06FDXN3ZBRS6JK7Q8G6JVSPXFC', 'Per-repo Claude account: spawn-time CLAUDE_CONFIG_DIR injection', 'The load-bearing piece of the multi-account epic (T-476): get the right `CLAUDE_CONFIG_DIR` into the hosted Claude process''s environment. Once this is in, a workspace bound to an account spawns its `claude` under that account end-to-end.
|
||||||
|
|
||||||
|
## Where
|
||||||
|
|
||||||
|
One resolver + one hook — pattern follows `lib/src/env/shell_env.dart` (single source of truth for env-related concerns).
|
||||||
|
|
||||||
|
### New resolver
|
||||||
|
|
||||||
|
`String? claudeConfigDirForWorkspace(String cwd)`:
|
||||||
|
|
||||||
|
1. Look up the binding via the T-477 `AccountRegistry`.
|
||||||
|
2. If bound → return the account''s `configDir`.
|
||||||
|
3. If unbound → return `Platform.environment[''CLAUDE_CONFIG_DIR'']` when the parent already set it (respect launcher choice); otherwise `null` (Claude defaults to `~/.claude`).
|
||||||
|
|
||||||
|
Flutter-free, pure (registry injected), unit-tested.
|
||||||
|
|
||||||
|
### Injection point
|
||||||
|
|
||||||
|
`lib/builtin/claude/src/agent_bootstrap.dart`''s `agentBootstrap(workspaceRoot, base:)`: when the resolver returns non-null, merge `''CLAUDE_CONFIG_DIR'': <path>` into the returned `envDelta` BEFORE the `base` spread, so `SpawnSpec.env` keeps its per-call-override precedence (line 67 of `session_orchestrator.dart` already passes the override as `base`).
|
||||||
|
|
||||||
|
Every hosted session (primary / secondary / fork / teammate) inherits the binding because `ClaudeSessionOrchestrator._spawn` already routes through `agentBootstrap` at `session_orchestrator.dart:247` — no per-call-site work.
|
||||||
|
|
||||||
|
## Acceptance
|
||||||
|
|
||||||
|
1. A bound workspace spawns `claude` with `CLAUDE_CONFIG_DIR=<account-dir>` — verifiable by `ps eww` / `lsof` against the spawned process.
|
||||||
|
2. An unbound workspace spawns identically to today — no `CLAUDE_CONFIG_DIR` injected unless the parent env already had one.
|
||||||
|
3. `SpawnSpec.env` per-call override still wins (precedence: explicit override > workspace binding > parent env > unset).
|
||||||
|
4. The resolver returns `null` for unbound workspaces with no parent env; the bootstrap then omits the key (NOT writes an empty value).
|
||||||
|
5. Unit tests: resolver returns each of the four states; `envDelta` precedence is correct; agent_bootstrap unit tests already cover the merge shape — extend them.
|
||||||
|
|
||||||
|
## Depends on
|
||||||
|
|
||||||
|
- T-477 (storage). The resolver needs `AccountRegistry.accountForWorkspace(cwd)`.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Restarting an already-running session on binding change — that lives in T-480''s `set` verb (it owns the close-then-spawn flow).
|
||||||
|
- IDE bridge discovery for the bound account — T-479.
|
||||||
|
', 'done', 'medium', NULL, NULL, NULL, '2026-06-25 09:16:42', '2026-06-27 14:59:54.882', NULL, 'e66f082fc7dc8dcca681eaa62be4e507', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||||
|
|||||||
@@ -74,6 +74,21 @@ Map<String, String> agentEnvDelta({required String workspaceRoot, required Strin
|
|||||||
return delta;
|
return delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the `CLAUDE_CONFIG_DIR` a session in [cwd] should run under (T-484,
|
||||||
|
/// epic T-476): the bound account's dir when the workspace is bound, else the
|
||||||
|
/// parent's `CLAUDE_CONFIG_DIR` when the launcher already set one, else null
|
||||||
|
/// (Claude defaults to `~/.claude`).
|
||||||
|
///
|
||||||
|
/// Pure: the AccountRegistry is injected as a plain [boundConfigDir] lookup
|
||||||
|
/// (workspace → bound config dir, or null) so this stays Flutter-free — the
|
||||||
|
/// registry itself lives behind a ChangeNotifier the orchestrator owns.
|
||||||
|
String? claudeConfigDirForWorkspace({required String cwd, required String? Function(String cwd) boundConfigDir, required Map<String, String> env}) {
|
||||||
|
final bound = boundConfigDir(cwd);
|
||||||
|
if (bound != null && bound.isNotEmpty) return bound;
|
||||||
|
final inherited = env['CLAUDE_CONFIG_DIR'];
|
||||||
|
return (inherited != null && inherited.isNotEmpty) ? inherited : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Locate the directory to prepend to a hosted agent's PATH so `clide`
|
/// Locate the directory to prepend to a hosted agent's PATH so `clide`
|
||||||
/// resolves (T-215). Returns null when `clide` is ALREADY on [currentPath]
|
/// resolves (T-215). Returns null when `clide` is ALREADY on [currentPath]
|
||||||
/// (the installed case — T-211 drops it in `~/.local/bin`, normally already
|
/// (the installed case — T-211 drops it in `~/.local/bin`, normally already
|
||||||
@@ -107,7 +122,7 @@ class AgentBootstrap {
|
|||||||
/// env (usually null → inherit clide's). The returned [AgentBootstrap.extraArgs]
|
/// env (usually null → inherit clide's). The returned [AgentBootstrap.extraArgs]
|
||||||
/// carries the context note; team callers append their own preamble and the
|
/// carries the context note; team callers append their own preamble and the
|
||||||
/// orchestrator merges both into one `--append-system-prompt`.
|
/// orchestrator merges both into one `--append-system-prompt`.
|
||||||
AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base}) {
|
AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base, String? Function(String cwd)? boundConfigDir}) {
|
||||||
final home = Platform.environment['HOME'];
|
final home = Platform.environment['HOME'];
|
||||||
// The login-shell-resolved PATH (T-439) so a hosted claude — and the tools it
|
// The login-shell-resolved PATH (T-439) so a hosted claude — and the tools it
|
||||||
// shells out to — find user-installed components on a desktop launch, not just
|
// shells out to — find user-installed components on a desktop launch, not just
|
||||||
@@ -120,7 +135,12 @@ AgentBootstrap agentBootstrap(String workspaceRoot, {Map<String, String>? base})
|
|||||||
];
|
];
|
||||||
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);
|
final cliDir = resolveClideCliDir(currentPath: currentPath, candidateDirs: candidates, isExecutableFile: _isExecutableFile);
|
||||||
final delta = agentEnvDelta(workspaceRoot: workspaceRoot, socketPath: workspaceSocketPath(workspaceRoot), currentPath: currentPath, clideCliDir: cliDir);
|
final delta = agentEnvDelta(workspaceRoot: workspaceRoot, socketPath: workspaceSocketPath(workspaceRoot), currentPath: currentPath, clideCliDir: cliDir);
|
||||||
return AgentBootstrap(envDelta: {...?base, ...delta}, extraArgs: ['--allowedTools', clideBashAllowRule]);
|
// Per-repo Claude account (T-484): a bound workspace runs claude under that
|
||||||
|
// account's CLAUDE_CONFIG_DIR. Spread BEFORE base so an explicit per-call
|
||||||
|
// SpawnSpec.env override still wins (precedence: override > binding > parent
|
||||||
|
// env > unset); omitted entirely when there's nothing to set.
|
||||||
|
final configDir = claudeConfigDirForWorkspace(cwd: workspaceRoot, boundConfigDir: boundConfigDir ?? (_) => null, env: Platform.environment);
|
||||||
|
return AgentBootstrap(envDelta: {'CLAUDE_CONFIG_DIR': ?configDir, ...?base, ...delta}, extraArgs: ['--allowedTools', clideBashAllowRule]);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isExecutableFile(String path) {
|
bool _isExecutableFile(String path) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:clide/clide.dart';
|
import 'package:clide/clide.dart';
|
||||||
|
import 'package:clide/builtin/claude/src/account_registry.dart';
|
||||||
import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel;
|
import 'package:clide/builtin/claude/src/activity_cluster.dart' show foldLevelFromName, kActivityFoldLevelKey, nextFoldLevel;
|
||||||
import 'package:clide/builtin/claude/src/claude_config.dart';
|
import 'package:clide/builtin/claude/src/claude_config.dart';
|
||||||
import 'package:clide/builtin/claude/src/claude_status.dart' show nextSafePermissionMode;
|
import 'package:clide/builtin/claude/src/claude_status.dart' show nextSafePermissionMode;
|
||||||
@@ -467,8 +468,9 @@ class ClaudeExtension extends ClideExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The clide-managed session set (T-169). Panes spawn/bind through it so a
|
// The clide-managed session set (T-169). Panes spawn/bind through it so a
|
||||||
// session outlives its pane and is shared across surfaces.
|
// session outlives its pane and is shared across surfaces. The account
|
||||||
_orchestrator = ClaudeSessionOrchestrator();
|
// registry (T-476) lets a bound workspace spawn under its own Claude account.
|
||||||
|
_orchestrator = ClaudeSessionOrchestrator(accountRegistry: AccountRegistry(ctx.settings));
|
||||||
activeSessionOrchestrator = _orchestrator;
|
activeSessionOrchestrator = _orchestrator;
|
||||||
|
|
||||||
// An in-place workspace switch (Open Project/Folder) must not leave the
|
// An in-place workspace switch (Open Project/Folder) must not leave the
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:clide/builtin/claude/src/account_registry.dart';
|
||||||
import 'package:clide/builtin/claude/src/agent_bootstrap.dart';
|
import 'package:clide/builtin/claude/src/agent_bootstrap.dart';
|
||||||
import 'package:clide/builtin/claude/src/conversation_controller.dart';
|
import 'package:clide/builtin/claude/src/conversation_controller.dart';
|
||||||
import 'package:clide/builtin/claude/src/session_naming.dart';
|
import 'package:clide/builtin/claude/src/session_naming.dart';
|
||||||
@@ -152,10 +153,15 @@ class ManagedSession {
|
|||||||
ClaudeSessionOrchestrator? activeSessionOrchestrator;
|
ClaudeSessionOrchestrator? activeSessionOrchestrator;
|
||||||
|
|
||||||
class ClaudeSessionOrchestrator extends ChangeNotifier {
|
class ClaudeSessionOrchestrator extends ChangeNotifier {
|
||||||
ClaudeSessionOrchestrator({ProcessFactory? processFactory}) : _factory = processFactory ?? _spawnClaude {
|
ClaudeSessionOrchestrator({ProcessFactory? processFactory, this.accountRegistry}) : _factory = processFactory ?? _spawnClaude {
|
||||||
_chatModel = TeamChatModel(broker: broker, sessionResolver: (name) => byMemberName(name)?.session);
|
_chatModel = TeamChatModel(broker: broker, sessionResolver: (name) => byMemberName(name)?.session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-repo Claude account bindings (epic T-476). When a workspace is bound,
|
||||||
|
/// its hosted sessions spawn under that account's CLAUDE_CONFIG_DIR (T-484).
|
||||||
|
/// Null in tests / when no registry is wired → no injection.
|
||||||
|
final AccountRegistry? accountRegistry;
|
||||||
|
|
||||||
final ProcessFactory _factory;
|
final ProcessFactory _factory;
|
||||||
final _sessions = <String, ManagedSession>{};
|
final _sessions = <String, ManagedSession>{};
|
||||||
|
|
||||||
@@ -244,7 +250,7 @@ class ClaudeSessionOrchestrator extends ChangeNotifier {
|
|||||||
mcpServers.add(TeamMcpServer(broker: broker, memberId: spec.id));
|
mcpServers.add(TeamMcpServer(broker: broker, memberId: spec.id));
|
||||||
preambles.add(_teamSystemPrompt(name, spec.role));
|
preambles.add(_teamSystemPrompt(name, spec.role));
|
||||||
}
|
}
|
||||||
final bootstrap = agentBootstrap(spec.cwd, base: spec.env);
|
final bootstrap = agentBootstrap(spec.cwd, base: spec.env, boundConfigDir: (cwd) => accountRegistry?.accountForWorkspace(cwd)?.dir);
|
||||||
sessionArgs = [
|
sessionArgs = [
|
||||||
'--append-system-prompt',
|
'--append-system-prompt',
|
||||||
preambles.join('\n\n'),
|
preambles.join('\n\n'),
|
||||||
|
|||||||
@@ -83,6 +83,32 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('claudeConfigDirForWorkspace (T-484)', () {
|
||||||
|
test('bound workspace → the account config dir', () {
|
||||||
|
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => '/home/u/.claude-work', env: const {}), '/home/u/.claude-work');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unbound + parent CLAUDE_CONFIG_DIR set → respects the launcher choice', () {
|
||||||
|
expect(
|
||||||
|
claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => null, env: const {'CLAUDE_CONFIG_DIR': '/home/u/.claude-personal'}),
|
||||||
|
'/home/u/.claude-personal',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unbound + no parent env → null (Claude defaults to ~/.claude)', () {
|
||||||
|
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => null, env: const {}), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('binding beats the parent env (binding > parent)', () {
|
||||||
|
expect(claudeConfigDirForWorkspace(cwd: '/repo/a', boundConfigDir: (_) => '/bound', env: const {'CLAUDE_CONFIG_DIR': '/parent'}), '/bound');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an empty bound dir is ignored (falls through to parent / null)', () {
|
||||||
|
expect(claudeConfigDirForWorkspace(cwd: '/r', boundConfigDir: (_) => '', env: const {'CLAUDE_CONFIG_DIR': '/parent'}), '/parent');
|
||||||
|
expect(claudeConfigDirForWorkspace(cwd: '/r', boundConfigDir: (_) => '', env: const {}), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('agentBootstrap (IO wrapper)', () {
|
group('agentBootstrap (IO wrapper)', () {
|
||||||
test('socket in the delta matches workspaceSocketPath; allow rule in extraArgs', () {
|
test('socket in the delta matches workspaceSocketPath; allow rule in extraArgs', () {
|
||||||
final b = agentBootstrap('/some/workspace');
|
final b = agentBootstrap('/some/workspace');
|
||||||
@@ -96,5 +122,15 @@ void main() {
|
|||||||
expect(b.envDelta['FOO'], 'bar');
|
expect(b.envDelta['FOO'], 'bar');
|
||||||
expect(b.envDelta['CLIDE_WORKSPACE'], '/ws');
|
expect(b.envDelta['CLIDE_WORKSPACE'], '/ws');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('injects the bound workspace CLAUDE_CONFIG_DIR (T-484)', () {
|
||||||
|
final b = agentBootstrap('/ws', boundConfigDir: (_) => '/home/u/.claude-work');
|
||||||
|
expect(b.envDelta['CLAUDE_CONFIG_DIR'], '/home/u/.claude-work');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('an explicit base CLAUDE_CONFIG_DIR override beats the workspace binding (T-484)', () {
|
||||||
|
final b = agentBootstrap('/ws', base: {'CLAUDE_CONFIG_DIR': '/override'}, boundConfigDir: (_) => '/bound');
|
||||||
|
expect(b.envDelta['CLAUDE_CONFIG_DIR'], '/override');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user