mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-21 07:21:46 +02:00
fix(hooks): honor CLAUDE_CONFIG_DIR instead of writing to ~/.claude
Claude Code points every profile-local path at CLAUDE_CONFIG_DIR when it is
set, but ECC hooks kept reading and writing ~/.claude. A session launched
under a second profile left its bash log, cost tracker, metrics, session data
and skill runs in the default profile, where the other one reads them back.
Two groups, as reported:
- getDefaultClaudeAgentDataHome() in scripts/lib/agent-data-home.js now
follows the variable, which covers every hook reaching ~/.claude through
resolveAgentDataHome(). ECC_AGENT_DATA_HOME still wins, so the documented
workaround keeps working.
- plan-canvas-pending, plan-canvas-sessions, mcp-health-check and
post-bash-command-log hard-coded os.homedir()/.claude and now resolve the
same way, matching scripts/hooks/ecc-statusline.js:68.
A blank value is ignored rather than resolved, since path.resolve(' ')
returns the process cwd and would scatter profile data through whatever
directory the session started in.
Fixes #3266
This commit is contained in:
@@ -47,11 +47,21 @@ function envNumber(name, fallback) {
|
||||
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
||||
}
|
||||
|
||||
// Claude Code redirects every profile-local path when CLAUDE_CONFIG_DIR is set.
|
||||
// Writing to ~/.claude regardless leaks this profile's data into the default one.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (configured && String(configured).trim().length > 0) {
|
||||
return path.resolve(String(configured).trim());
|
||||
}
|
||||
return path.join(os.homedir(), '.claude');
|
||||
}
|
||||
|
||||
function stateFilePath() {
|
||||
if (process.env.ECC_MCP_HEALTH_STATE_PATH) {
|
||||
return path.resolve(process.env.ECC_MCP_HEALTH_STATE_PATH);
|
||||
}
|
||||
return path.join(os.homedir(), '.claude', 'mcp-health-cache.json');
|
||||
return path.join(claudeConfigDir(), 'mcp-health-cache.json');
|
||||
}
|
||||
|
||||
function configPaths() {
|
||||
|
||||
@@ -34,10 +34,20 @@ const path = require('path');
|
||||
const SERVER_TIMEOUT_MS = 1000;
|
||||
const MAX_ITEMS_REPORTED = 20;
|
||||
|
||||
// Claude Code redirects every profile-local path when CLAUDE_CONFIG_DIR is set.
|
||||
// Writing to ~/.claude regardless leaks this profile's data into the default one.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (configured && String(configured).trim().length > 0) {
|
||||
return path.resolve(String(configured).trim());
|
||||
}
|
||||
return path.join(os.homedir(), '.claude');
|
||||
}
|
||||
|
||||
function stateDir() {
|
||||
const override = process.env.ECC_PLAN_CANVAS_STATE_DIR;
|
||||
if (override && override.trim()) return path.resolve(override.trim());
|
||||
return path.join(os.homedir(), '.claude', 'plan-canvas');
|
||||
return path.join(claudeConfigDir(), 'plan-canvas');
|
||||
}
|
||||
|
||||
function readState() {
|
||||
|
||||
@@ -19,10 +19,20 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Claude Code redirects every profile-local path when CLAUDE_CONFIG_DIR is set.
|
||||
// Writing to ~/.claude regardless leaks this profile's data into the default one.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (configured && String(configured).trim().length > 0) {
|
||||
return path.resolve(String(configured).trim());
|
||||
}
|
||||
return path.join(os.homedir(), '.claude');
|
||||
}
|
||||
|
||||
function stateDir() {
|
||||
const override = process.env.ECC_PLAN_CANVAS_STATE_DIR;
|
||||
if (override && override.trim()) return path.resolve(override.trim());
|
||||
return path.join(os.homedir(), '.claude', 'plan-canvas');
|
||||
return path.join(claudeConfigDir(), 'plan-canvas');
|
||||
}
|
||||
|
||||
function openSessions() {
|
||||
|
||||
@@ -38,6 +38,16 @@ function appendLine(filePath, line) {
|
||||
fs.appendFileSync(filePath, `${line}\n`, 'utf8');
|
||||
}
|
||||
|
||||
// Claude Code redirects every profile-local path when CLAUDE_CONFIG_DIR is set.
|
||||
// Writing to ~/.claude regardless leaks this profile's data into the default one.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
if (configured && String(configured).trim().length > 0) {
|
||||
return path.resolve(String(configured).trim());
|
||||
}
|
||||
return path.join(os.homedir(), '.claude');
|
||||
}
|
||||
|
||||
function run(rawInput, mode = 'audit') {
|
||||
const config = MODE_CONFIG[mode];
|
||||
|
||||
@@ -45,7 +55,7 @@ function run(rawInput, mode = 'audit') {
|
||||
if (config) {
|
||||
const input = String(rawInput || '').trim() ? JSON.parse(String(rawInput)) : {};
|
||||
const command = sanitizeCommand(input.tool_input?.command || '?');
|
||||
appendLine(path.join(os.homedir(), '.claude', config.fileName), config.format(command));
|
||||
appendLine(path.join(claudeConfigDir(), config.fileName), config.format(command));
|
||||
}
|
||||
} catch {
|
||||
// Logging must never block the calling hook.
|
||||
|
||||
@@ -17,6 +17,7 @@ const path = require('path');
|
||||
const { assertWithinTrustedRoot } = require('./path-safety');
|
||||
|
||||
const AGENT_DATA_HOME_ENV = 'ECC_AGENT_DATA_HOME';
|
||||
const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
|
||||
const DEFAULT_CLAUDE_DIR_NAME = '.claude';
|
||||
const DEFAULT_CURSOR_ECC_DIR_SEGMENTS = ['.cursor', 'ecc'];
|
||||
const PROJECT_CONFIG_RELATIVE = path.join('.cursor', 'ecc-agent-data.json');
|
||||
@@ -92,6 +93,15 @@ function getDefaultCursorAgentDataHome() {
|
||||
}
|
||||
|
||||
function getDefaultClaudeAgentDataHome() {
|
||||
// Claude Code points every profile-local path at CLAUDE_CONFIG_DIR when it is
|
||||
// set, so a session launched under a second profile must not keep reading and
|
||||
// writing the default ~/.claude. This moves the trusted root with it, which is
|
||||
// the same trust ECC_AGENT_DATA_HOME already gets: both come from whoever
|
||||
// launched the session, not from the project.
|
||||
const configDir = process.env[CLAUDE_CONFIG_DIR_ENV];
|
||||
if (configDir && String(configDir).trim().length > 0) {
|
||||
return expandHomePath(String(configDir).trim(), process.cwd());
|
||||
}
|
||||
return path.join(getHomeDirFromEnv(), DEFAULT_CLAUDE_DIR_NAME);
|
||||
}
|
||||
|
||||
@@ -228,6 +238,7 @@ function getCursorSessionEnvPayload(options = {}) {
|
||||
|
||||
module.exports = {
|
||||
AGENT_DATA_HOME_ENV,
|
||||
CLAUDE_CONFIG_DIR_ENV,
|
||||
DEFAULT_CLAUDE_DIR_NAME,
|
||||
DEFAULT_CURSOR_ECC_DIR_SEGMENTS,
|
||||
PROJECT_CONFIG_RELATIVE,
|
||||
|
||||
@@ -497,6 +497,50 @@ function runTests() {
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getDefaultClaudeAgentDataHome follows CLAUDE_CONFIG_DIR', () => {
|
||||
// Claude Code points every profile-local path at CLAUDE_CONFIG_DIR. Ignoring
|
||||
// it writes this profile's session data, logs and metrics into the default
|
||||
// ~/.claude, where another profile reads them back.
|
||||
withIsolatedCwd(() => {
|
||||
withEnv({
|
||||
CLAUDE_CONFIG_DIR: path.join(os.tmpdir(), 'ecc-profile-x'),
|
||||
ECC_AGENT_DATA_HOME: undefined,
|
||||
}, () => {
|
||||
const agentDataHome = require('../../scripts/lib/agent-data-home');
|
||||
assert.strictEqual(
|
||||
agentDataHome.getDefaultClaudeAgentDataHome(),
|
||||
path.join(os.tmpdir(), 'ecc-profile-x')
|
||||
);
|
||||
});
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('getDefaultClaudeAgentDataHome falls back to ~/.claude without the env', () => {
|
||||
withIsolatedCwd(() => {
|
||||
withEnv({ CLAUDE_CONFIG_DIR: undefined, ECC_AGENT_DATA_HOME: undefined }, () => {
|
||||
const agentDataHome = require('../../scripts/lib/agent-data-home');
|
||||
assert.strictEqual(
|
||||
agentDataHome.getDefaultClaudeAgentDataHome(),
|
||||
path.join(os.homedir(), '.claude')
|
||||
);
|
||||
});
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('a blank CLAUDE_CONFIG_DIR is ignored rather than resolving to cwd', () => {
|
||||
// An empty or whitespace value would otherwise resolve to the process cwd and
|
||||
// scatter profile data through whatever directory the session started in.
|
||||
withIsolatedCwd(() => {
|
||||
withEnv({ CLAUDE_CONFIG_DIR: ' ', ECC_AGENT_DATA_HOME: undefined }, () => {
|
||||
const agentDataHome = require('../../scripts/lib/agent-data-home');
|
||||
assert.strictEqual(
|
||||
agentDataHome.getDefaultClaudeAgentDataHome(),
|
||||
path.join(os.homedir(), '.claude')
|
||||
);
|
||||
});
|
||||
});
|
||||
})) passed++; else failed++;
|
||||
|
||||
console.log(`\n=== Test Results ===\nPassed: ${passed}\nFailed: ${failed}\n`);
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user