Expand a leading ~ in IMPECCABLE_CACHE_ROOT against the home dir

Env files and settings JSON hand '~/caches' to Node unexpanded; without
this it would resolve to a literal '~' directory under the process cwd.
Mirrors the exact treatment IMPECCABLE_HOOK_LOG already gets in
writeAuditLog (HOME || USERPROFILE fallback), plus the Windows '~\'
spelling.

Prepared with AI assistance (Claude Code) under direction of
0xDarkMatter, per the maintainer-approved issue #422.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
0xDarkMatter
2026-08-28 15:14:49 +05:00
committed by Abdul Wahab
co-authored by Claude Fable 5
parent 5c82d58b7e
commit 30b3628f5b
2 changed files with 29 additions and 4 deletions
+9 -4
View File
@@ -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);
+20
View File
@@ -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);