Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions __tests__/installer-targets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,24 @@ function setHome(dir: string): { restore: () => void } {
APPDATA: process.env.APPDATA,
XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
HERMES_HOME: process.env.HERMES_HOME,
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
};
process.env.HOME = dir;
process.env.USERPROFILE = dir;
process.env.APPDATA = path.join(dir, '.config');
process.env.XDG_CONFIG_HOME = path.join(dir, '.config');
delete process.env.HERMES_HOME;
// Clear CLAUDE_CONFIG_DIR so a custom profile in the real environment
// doesn't leak into tests that assume the default ~/.claude profile.
delete process.env.CLAUDE_CONFIG_DIR;
return {
restore() {
if (prev.HOME === undefined) delete process.env.HOME; else process.env.HOME = prev.HOME;
if (prev.USERPROFILE === undefined) delete process.env.USERPROFILE; else process.env.USERPROFILE = prev.USERPROFILE;
if (prev.APPDATA === undefined) delete process.env.APPDATA; else process.env.APPDATA = prev.APPDATA;
if (prev.XDG_CONFIG_HOME === undefined) delete process.env.XDG_CONFIG_HOME; else process.env.XDG_CONFIG_HOME = prev.XDG_CONFIG_HOME;
if (prev.HERMES_HOME === undefined) delete process.env.HERMES_HOME; else process.env.HERMES_HOME = prev.HERMES_HOME;
if (prev.CLAUDE_CONFIG_DIR === undefined) delete process.env.CLAUDE_CONFIG_DIR; else process.env.CLAUDE_CONFIG_DIR = prev.CLAUDE_CONFIG_DIR;
},
};
}
Expand Down Expand Up @@ -921,6 +926,31 @@ describe('Installer targets — partial-state idempotency', () => {
expect(cfg.mcpServers.codegraph).toBeDefined();
});

it('claude: global install honors CLAUDE_CONFIG_DIR, leaving ~/.claude untouched', () => {
const claude = getTarget('claude')!;
const profile = path.join(tmpHome, '.claude-gc');
const prev = process.env.CLAUDE_CONFIG_DIR;
process.env.CLAUDE_CONFIG_DIR = profile;
try {
claude.install('global', { autoAllow: true });

// MCP entry, settings.json, and CLAUDE.md all land in the custom
// profile dir — Claude Code keeps .claude.json *inside* it when the
// env var is set.
const mcp = JSON.parse(fs.readFileSync(path.join(profile, '.claude.json'), 'utf-8'));
expect(mcp.mcpServers.codegraph).toBeDefined();
expect(fs.existsSync(path.join(profile, 'settings.json'))).toBe(true);
expect(fs.existsSync(path.join(profile, 'CLAUDE.md'))).toBe(true);

// The default profile must be left alone.
expect(fs.existsSync(path.join(tmpHome, '.claude.json'))).toBe(false);
expect(fs.existsSync(path.join(tmpHome, '.claude'))).toBe(false);
} finally {
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prev;
}
});

it('claude: local install migrates a legacy ./.claude.json codegraph entry into ./.mcp.json', () => {
const claude = getTarget('claude')!;
const legacy = path.join(tmpCwd, '.claude.json');
Expand Down
30 changes: 24 additions & 6 deletions src/installer/targets/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,36 @@ import {
CODEGRAPH_SECTION_START,
} from '../instructions-template';

/**
* Root of the global Claude Code profile. Honors CLAUDE_CONFIG_DIR so
* non-default profiles (e.g. `~/.claude-gc`) get configured instead of
* `~/.claude` — Claude Code reads settings.json, CLAUDE.md, and (when
* the env var is set) .claude.json from this directory. Only affects the
* global location; project-local files are unaffected by the env var.
*/
function globalConfigDir(): string {
const cfg = process.env.CLAUDE_CONFIG_DIR;
return cfg && cfg.trim().length > 0
? path.resolve(cfg)
: path.join(os.homedir(), '.claude');
}
function configDir(loc: Location): string {
return loc === 'global'
? path.join(os.homedir(), '.claude')
? globalConfigDir()
: path.join(process.cwd(), '.claude');
}
function mcpJsonPath(loc: Location): string {
// global → ~/.claude.json (user scope: visible in every project).
// global → $CLAUDE_CONFIG_DIR/.claude.json when that env var is set
// (Claude Code keeps .claude.json inside a custom profile dir), else
// ~/.claude.json in HOME (the default profile keeps it beside, not
// inside, ~/.claude). User scope: visible in every project.
// local → ./.mcp.json (project scope: the ONLY project-level MCP
// file Claude Code reads — NOT ./.claude.json, which it ignores).
return loc === 'global'
? path.join(os.homedir(), '.claude.json')
: path.join(process.cwd(), '.mcp.json');
// file Claude Code reads — NOT ./.claude.json, which it ignores).
if (loc !== 'global') return path.join(process.cwd(), '.mcp.json');
const cfg = process.env.CLAUDE_CONFIG_DIR;
return cfg && cfg.trim().length > 0
? path.join(path.resolve(cfg), '.claude.json')
: path.join(os.homedir(), '.claude.json');
}
/**
* Where pre-#207 installers wrote the local MCP entry. Claude Code
Expand Down