feat(install): per-instance data dir isolation via --data-dir / --instance (#193)#231
feat(install): per-instance data dir isolation via --data-dir / --instance (#193)#231yashrajsapra wants to merge 13 commits into
Conversation
…tance + workspace CLI (closes #193) - `apra-fleet install --data-dir <path>` — passes APRA_FLEET_DATA_DIR to the MCP server env for Claude (-e flag), and embeds env in settings.json for Gemini/ Codex/Copilot providers - `apra-fleet install --instance <name>` — shorthand that sets data-dir to ~/.apra-fleet/workspaces/<name>, registers MCP server as apra-fleet-<name>, and writes the workspace entry to workspaces.json - `apra-fleet workspace list/add/remove/use/status` — new workspace management CLI for listing, creating, activating, and inspecting named workspaces - `mergePermissions` uses the correct mcp__<serverName>__* permission pattern - `src/paths.ts` adds APRA_BASE, WORKSPACES_DIR, WORKSPACES_INDEX constants - 16 new tests covering all flag combinations across Claude, Gemini, and Copilot Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
6fe912e to
3efca15
Compare
| if (llm === 'claude') { | ||
| try { | ||
| run('claude mcp remove apra-fleet --scope user', { stdio: 'ignore' }); | ||
| run(`claude mcp remove ${serverName} --scope user`, { stdio: 'ignore' }); |
There was a problem hiding this comment.
MEDIUM (non-blocking): mcp remove still goes through run(), which calls execSync with the command as a shell string. serverName is validated upstream so this is safe in practice, but it is inconsistent with the execFileSync argv-array fix applied everywhere else in this PR.
Consider splitting run() into a runFile(bin, args[]) variant, or inlining this call as:
execFileSync('claude', ['mcp', 'remove', serverName, '--scope', 'user'], { stdio: 'ignore' });This keeps the security posture consistent across the whole install path.
| import os from 'node:os'; | ||
|
|
||
| const home = os.homedir(); | ||
| const APRA_BASE = path.join(home, '.apra-fleet'); |
There was a problem hiding this comment.
LOW (non-blocking): APRA_BASE and DEFAULT_DATA_DIR are re-derived here from scratch, but paths.ts already exports APRA_BASE and FLEET_DIR (which is APRA_FLEET_DATA_DIR ?? .apra-fleet/data). Importing from paths.ts would eliminate the duplication and ensure both files always agree on the canonical base path — especially important if the default ever changes.
kumaakh
left a comment
There was a problem hiding this comment.
BLOCKING — apra-fleet update silently loses custom workspace on replay
Two issues in this section mean that apra-fleet update will always reinstall to the default workspace, silently discarding any --data-dir / --instance the user originally passed.
Issue 1 (line 678): installConfig only saves { llm, skill }. dataDir and instanceName are not persisted.
Issue 2 (line 679): configDir is hardcoded to FLEET_BASE/data (i.e. ~/.apra-fleet/data/). When a custom --data-dir is used, the config is written to the wrong directory — the default workspace's data folder instead of the custom one.
Issue 3 (not in this diff — src/cli/update.ts line 91): Even if installConfig were fixed to save dataDir, the update replay only constructs ['install', '--llm', config.llm, '--skill', config.skill] — --data-dir and --instance are never read back or passed.
Fix needed in this PR:
// line 678 — include dataDir / instanceName
const installConfig: Record<string, string> = { llm, skill: skillMode };
if (dataDir) installConfig.dataDir = dataDir;
if (instanceName) installConfig.instance = instanceName;
// line 679 — write config to the active data dir, not hardcoded default
const configDir = dataDir ?? path.join(FLEET_BASE, 'data');And update.ts must be updated to read config.dataDir / config.instance and append --data-dir / --instance to the replay args.
| mergePermissions(paths, serverName); | ||
|
|
||
| // Write install-config.json | ||
| const installConfig = { llm, skill: skillMode }; |
There was a problem hiding this comment.
BLOCKING — installConfig omits dataDir and instanceName. When apra-fleet update reads this file to replay the install, it has no record of the custom data dir and will reinstall for the default workspace only, silently breaking any non-default workspace install. See review body for the full issue and suggested fix.
|
Design suggestion for the Rather than a per-workspace file, {
"installs": [
{ "llm": "claude", "skill": "all" },
{ "llm": "claude", "skill": "fleet", "instance": "work" },
{ "llm": "gemini", "skill": "fleet", "dataDir": "/custom/path" }
]
}Each entry maps 1:1 to a CLI invocation. Upsert rule: key on Changes needed:
|
Summary
--data-dirand--instanceflags toinstallso multiple Claude Code instances running on the same machine use isolated config/log directories instead of fighting over a shared one--data-diris eliminated by switching toexecFileSyncargv array (security fix surfaced in review)Changes
feat(install): per-instance data dir isolation via--data-dir/--instance+ workspace CLIdocs: multi-instance usage guidefix(install): eliminate shell injection in--data-dirviaexecFileSyncargv arraychore: cleanup fleet control files (PLAN.md, progress.json, feedback.md, CLAUDE.md, AGENTS.md)Test plan
node dist/index.js install --data-dir /tmp/inst-aand--data-dir /tmp/inst-bproduce fully isolated runtime dirsnode dist/index.js install --instance myprojectresolves to a deterministic subdirectory--data-dirdoes not cause shell injection--data-dircontinue to use the default data directory unchanged🤖 Generated with Claude Code