Skip to content

feat(install): per-instance data dir isolation via --data-dir / --instance (#193)#231

Open
yashrajsapra wants to merge 13 commits into
mainfrom
feat/per-instance-data-dir
Open

feat(install): per-instance data dir isolation via --data-dir / --instance (#193)#231
yashrajsapra wants to merge 13 commits into
mainfrom
feat/per-instance-data-dir

Conversation

@yashrajsapra
Copy link
Copy Markdown
Contributor

Summary

  • Adds --data-dir and --instance flags to install so multiple Claude Code instances running on the same machine use isolated config/log directories instead of fighting over a shared one
  • Shell injection in --data-dir is eliminated by switching to execFileSync argv array (security fix surfaced in review)
  • Multi-instance usage guide added to docs

Changes

  • feat(install): per-instance data dir isolation via --data-dir / --instance + workspace CLI
  • docs: multi-instance usage guide
  • fix(install): eliminate shell injection in --data-dir via execFileSync argv array
  • chore: 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-a and --data-dir /tmp/inst-b produce fully isolated runtime dirs
  • node dist/index.js install --instance myproject resolves to a deterministic subdirectory
  • Passing a path with spaces / special chars in --data-dir does not cause shell injection
  • Existing installs without --data-dir continue to use the default data directory unchanged
  • Multi-instance usage guide renders correctly in docs

🤖 Generated with Claude Code

@yashrajsapra yashrajsapra requested a review from kumaakh May 3, 2026 10:48
yashrajsapra and others added 9 commits May 3, 2026 16:22
…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>
@yashrajsapra yashrajsapra force-pushed the feat/per-instance-data-dir branch from 6fe912e to 3efca15 Compare May 3, 2026 10:57
@yashrajsapra yashrajsapra self-assigned this May 3, 2026
Comment thread src/cli/install.ts
if (llm === 'claude') {
try {
run('claude mcp remove apra-fleet --scope user', { stdio: 'ignore' });
run(`claude mcp remove ${serverName} --scope user`, { stdio: 'ignore' });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/cli/workspace.ts
import os from 'node:os';

const home = os.homedir();
const APRA_BASE = path.join(home, '.apra-fleet');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

@kumaakh kumaakh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/cli/install.ts
mergePermissions(paths, serverName);

// Write install-config.json
const installConfig = { llm, skill: skillMode };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BLOCKINGinstallConfig 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.

@kumaakh
Copy link
Copy Markdown
Contributor

kumaakh commented May 3, 2026

Design suggestion for the install-config.json fix

Rather than a per-workspace file, install-config.json should be a singleton accumulator at ~/.apra-fleet/install-config.json (outside any data dir) that tracks every install invocation:

{
  "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. apra-fleet update reads this one file and replays every entry in order — no need to discover workspaces via workspaces.json first.

Upsert rule: key on { instance, dataDir } (both optional/undefined for the default workspace). On re-install, replace the matching entry rather than append — so running apra-fleet install --instance work --llm gemini a second time updates that entry in place.

Changes needed:

  • src/cli/install.ts — after MCP registration, upsert into ~/.apra-fleet/install-config.json (not ${dataDir}/install-config.json)
  • src/cli/update.ts — read config.installs[], replay each entry with ['install', '--llm', e.llm, '--skill', e.skill, ...(e.instance ? ['--instance', e.instance] : []), ...(e.dataDir ? ['--data-dir', e.dataDir] : [])]
  • Tests: add a case that installs twice with different --instance values and verifies update would replay both

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants