mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-19 22:41:45 +02:00
Merge PR #3416: route hook state through Claude profiles
Preserve the explicit shared ~/.claude project root already shipped on main. Canonicalize macOS temporary workflow fixture paths.
This commit is contained in:
@@ -47,11 +47,29 @@ function envNumber(name, fallback) {
|
||||
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
||||
}
|
||||
|
||||
// Resolve the Claude profile directory the same way everywhere.
|
||||
//
|
||||
// Duplicated rather than imported on purpose: hooks ship in managed installs
|
||||
// that carry top-level `scripts/` without `scripts/lib/` (see #3259), so a
|
||||
// require into lib/ would make the hook unloadable there. `~` is expanded the
|
||||
// way `scripts/lib/agent-data-home.js` expands it, so a profile written as
|
||||
// `~/.claude-work` resolves to one place rather than two.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
const trimmed = configured ? String(configured).trim() : '';
|
||||
if (!trimmed) return path.join(os.homedir(), '.claude');
|
||||
if (trimmed.startsWith('~')) {
|
||||
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
||||
return remainder ? path.join(os.homedir(), remainder) : os.homedir();
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
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,28 @@ const path = require('path');
|
||||
const SERVER_TIMEOUT_MS = 1000;
|
||||
const MAX_ITEMS_REPORTED = 20;
|
||||
|
||||
// Resolve the Claude profile directory the same way everywhere.
|
||||
//
|
||||
// Duplicated rather than imported on purpose: hooks ship in managed installs
|
||||
// that carry top-level `scripts/` without `scripts/lib/` (see #3259), so a
|
||||
// require into lib/ would make the hook unloadable there. `~` is expanded the
|
||||
// way `scripts/lib/agent-data-home.js` expands it, so a profile written as
|
||||
// `~/.claude-work` resolves to one place rather than two.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
const trimmed = configured ? String(configured).trim() : '';
|
||||
if (!trimmed) return path.join(os.homedir(), '.claude');
|
||||
if (trimmed.startsWith('~')) {
|
||||
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
||||
return remainder ? path.join(os.homedir(), remainder) : os.homedir();
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
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,28 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
|
||||
// Resolve the Claude profile directory the same way everywhere.
|
||||
//
|
||||
// Duplicated rather than imported on purpose: hooks ship in managed installs
|
||||
// that carry top-level `scripts/` without `scripts/lib/` (see #3259), so a
|
||||
// require into lib/ would make the hook unloadable there. `~` is expanded the
|
||||
// way `scripts/lib/agent-data-home.js` expands it, so a profile written as
|
||||
// `~/.claude-work` resolves to one place rather than two.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
const trimmed = configured ? String(configured).trim() : '';
|
||||
if (!trimmed) return path.join(os.homedir(), '.claude');
|
||||
if (trimmed.startsWith('~')) {
|
||||
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
||||
return remainder ? path.join(os.homedir(), remainder) : os.homedir();
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
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,24 @@ function appendLine(filePath, line) {
|
||||
fs.appendFileSync(filePath, `${line}\n`, 'utf8');
|
||||
}
|
||||
|
||||
// Resolve the Claude profile directory the same way everywhere.
|
||||
//
|
||||
// Duplicated rather than imported on purpose: hooks ship in managed installs
|
||||
// that carry top-level `scripts/` without `scripts/lib/` (see #3259), so a
|
||||
// require into lib/ would make the hook unloadable there. `~` is expanded the
|
||||
// way `scripts/lib/agent-data-home.js` expands it, so a profile written as
|
||||
// `~/.claude-work` resolves to one place rather than two.
|
||||
function claudeConfigDir() {
|
||||
const configured = process.env.CLAUDE_CONFIG_DIR;
|
||||
const trimmed = configured ? String(configured).trim() : '';
|
||||
if (!trimmed) return path.join(os.homedir(), '.claude');
|
||||
if (trimmed.startsWith('~')) {
|
||||
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
||||
return remainder ? path.join(os.homedir(), remainder) : os.homedir();
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
function run(rawInput, mode = 'audit') {
|
||||
const config = MODE_CONFIG[mode];
|
||||
|
||||
@@ -45,7 +63,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,7 +93,9 @@ function getDefaultCursorAgentDataHome() {
|
||||
}
|
||||
|
||||
function getDefaultClaudeAgentDataHome() {
|
||||
const fromClaudeConfigDir = expandHomePath(process.env.CLAUDE_CONFIG_DIR);
|
||||
// Launcher-owned CLAUDE_CONFIG_DIR moves the Claude trusted root with its
|
||||
// profile. Project-provided paths must still remain inside that root.
|
||||
const fromClaudeConfigDir = expandHomePath(process.env[CLAUDE_CONFIG_DIR_ENV]);
|
||||
if (fromClaudeConfigDir) return fromClaudeConfigDir;
|
||||
return path.join(getHomeDirFromEnv(), DEFAULT_CLAUDE_DIR_NAME);
|
||||
}
|
||||
@@ -232,6 +235,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,
|
||||
|
||||
@@ -16,10 +16,26 @@ const path = require('path');
|
||||
const FEEDBACK_KINDS = new Set(['chat', 'annotation', 'verdict']);
|
||||
const VERDICTS = new Set(['approve', 'request-changes']);
|
||||
|
||||
// The producer and the SessionStart/Stop hooks must land on one directory. The
|
||||
// hooks resolve CLAUDE_CONFIG_DIR (they cannot require this file: managed
|
||||
// installs ship scripts/ without scripts/lib/, see #3259), so writing to
|
||||
// ~/.claude here regardless would hide queued feedback from them whenever a
|
||||
// session runs under a second profile.
|
||||
function resolveClaudeConfigDir(env = process.env) {
|
||||
const configured = env.CLAUDE_CONFIG_DIR;
|
||||
const trimmed = configured ? String(configured).trim() : '';
|
||||
if (!trimmed) return path.join(os.homedir(), '.claude');
|
||||
if (trimmed.startsWith('~')) {
|
||||
const remainder = trimmed.slice(1).replace(/^[/\\]+/, '');
|
||||
return remainder ? path.join(os.homedir(), remainder) : os.homedir();
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
}
|
||||
|
||||
function resolveStateDir(env = process.env) {
|
||||
const override = env.ECC_PLAN_CANVAS_STATE_DIR;
|
||||
if (override && String(override).trim()) return path.resolve(String(override).trim());
|
||||
return path.join(os.homedir(), '.claude', 'plan-canvas');
|
||||
return path.join(resolveClaudeConfigDir(env), 'plan-canvas');
|
||||
}
|
||||
|
||||
// Canonicalize so `./plan.md`, symlinks, and absolute paths all land on the
|
||||
|
||||
Reference in New Issue
Block a user