diff --git a/skill/scripts/hook-lib.mjs b/skill/scripts/hook-lib.mjs index fa5f2f1ef..91d3de7d9 100644 --- a/skill/scripts/hook-lib.mjs +++ b/skill/scripts/hook-lib.mjs @@ -220,12 +220,17 @@ export function getLocalConfigPath(cwd) { // disposable state relocates. // Read from process.env (not runHook's injected env): the cache root is a // machine-scoped setting like CURSOR_PROJECT_DIR, not a per-invocation -// switch. Trim guards against stray whitespace in env files; resolving both -// sides makes the slug deterministic when callers hand in a trailing -// separator or unnormalized cwd. +// switch. Trim guards against stray whitespace in env files; `~/` expands to +// the home dir (settings/env files hand it to Node unexpanded — same +// treatment IMPECCABLE_HOOK_LOG gets in writeAuditLog); resolving both sides +// makes the slug deterministic when callers hand in a trailing separator or +// unnormalized cwd. function hookStateDir(cwd) { const raw = process.env.IMPECCABLE_CACHE_ROOT; - const root = typeof raw === 'string' ? raw.trim() : ''; + let root = typeof raw === 'string' ? raw.trim() : ''; + if (root.startsWith('~/') || root.startsWith('~\\') || root === '~') { + root = path.join(process.env.HOME || process.env.USERPROFILE || '.', root.slice(2)); + } if (root) { const slug = path.resolve(String(cwd)).replace(/[:\\/.]/g, '-'); return path.join(path.resolve(root), slug); diff --git a/tests/hook.test.mjs b/tests/hook.test.mjs index 915ac063e..03aaf617c 100644 --- a/tests/hook.test.mjs +++ b/tests/hook.test.mjs @@ -495,6 +495,26 @@ describe('IMPECCABLE_CACHE_ROOT relocates hook state (issue #422)', () => { assert.equal(getLocalConfigPath(cwd), path.join(cwd, '.impeccable', 'config.local.json')); }); + it('expands a leading ~/ against the home dir, like IMPECCABLE_HOOK_LOG', () => { + const savedHome = process.env.HOME; + const savedProfile = process.env.USERPROFILE; + try { + process.env.HOME = cacheRoot; + delete process.env.USERPROFILE; + process.env.IMPECCABLE_CACHE_ROOT = '~/impeccable-state'; + const slug = path.resolve(cwd).replace(/[:\\/.]/g, '-'); + assert.equal( + getCachePath(cwd), + path.join(cacheRoot, 'impeccable-state', slug, 'hook.cache.json'), + ); + } finally { + if (savedHome === undefined) delete process.env.HOME; + else process.env.HOME = savedHome; + if (savedProfile === undefined) delete process.env.USERPROFILE; + else process.env.USERPROFILE = savedProfile; + } + }); + it('persistCache round-trips through the redirect dir and leaves the project root clean', () => { process.env.IMPECCABLE_CACHE_ROOT = cacheRoot; const cache = readCache(cwd);