Add IMPECCABLE_CACHE_ROOT to relocate hook state out of project roots (#422)

Honor an optional IMPECCABLE_CACHE_ROOT env var in getCachePath() /
getPendingPath(): when set, hook.cache.json and hook.pending.json land
under $IMPECCABLE_CACHE_ROOT/<project-slug>/ (slug = project path with
[:\/.] mapped to hyphens, mirroring Claude Code's ~/.claude/projects/
convention). Unset or blank env keeps stock project-local behavior.
User-authored config (config.json, config.local.json, design.json)
deliberately stays project-local - only disposable state relocates.

Also clears ambient IMPECCABLE_CACHE_ROOT at the top of hook.test.mjs so
a developer running the suite with the redirect active still gets
deterministic stock-path assertions; the new suite sets and restores the
var explicitly.

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 0c2517884d
commit 77a2eae861
2 changed files with 89 additions and 2 deletions
+19 -2
View File
@@ -210,12 +210,29 @@ export function getLocalConfigPath(cwd) {
return path.join(cwd, '.impeccable', 'config.local.json');
}
// Where mutable hook state (cache + pending) lives. Defaults to the
// project-local `.impeccable/` dir. When IMPECCABLE_CACHE_ROOT is set, state
// relocates to a per-project subdirectory of that root instead, keyed by a
// slug of the project path (`[:\\/.]` → `-`, mirroring Claude Code's
// `~/.claude/projects/` convention), so project roots stay free of tool
// artifacts (issue #422). User-authored config (config.json,
// config.local.json, design.json) deliberately stays project-local — only
// disposable state relocates.
function hookStateDir(cwd) {
const root = process.env.IMPECCABLE_CACHE_ROOT;
if (root && typeof root === 'string' && root.trim()) {
const slug = String(cwd).replace(/[:\\/.]/g, '-');
return path.join(root, slug);
}
return path.join(cwd, '.impeccable');
}
export function getCachePath(cwd) {
return path.join(cwd, '.impeccable', 'hook.cache.json');
return path.join(hookStateDir(cwd), 'hook.cache.json');
}
export function getPendingPath(cwd) {
return path.join(cwd, '.impeccable', 'hook.pending.json');
return path.join(hookStateDir(cwd), 'hook.pending.json');
}
export function resolveProjectCwd(event, fallback = process.cwd()) {