fix(paths): resolve the install root from CLAUDE_CONFIG_DIR / LIFEOS_DIR - #1918
Open
PigLardLord wants to merge 1 commit into
Open
fix(paths): resolve the install root from CLAUDE_CONFIG_DIR / LIFEOS_DIR#1918PigLardLord wants to merge 1 commit into
PigLardLord wants to merge 1 commit into
Conversation
LifeOS assumes it lives at $HOME/.claude. The harness does not — it exposes
CLAUDE_CONFIG_DIR precisely so the config dir can live elsewhere. On any install
that uses it, ~165 call sites resolved against a directory that was not the
install, and because settings.json, skills/, hooks/, agents/, projects/ and
CLAUDE.md exist in both trees the lookups succeeded against the wrong file
instead of failing.
The codebase already had the right pattern (Conduit/paths.ts,
checks/notification-governor.ts); it was applied unevenly, and notably not in
the shared resolver every hook imports.
- hooks/lib/paths.ts: getClaudeDir() honors CLAUDE_CONFIG_DIR, after
CLAUDE_PLUGIN_ROOT so plugin precedence is unchanged; getLifeosDir()'s
fallback derives from it instead of re-hardcoding ~/.claude
- 167 direct call sites across LIFEOS/** and hooks/** guarded on
process.env.LIFEOS_DIR / CLAUDE_CONFIG_DIR
- 16 plist templates parametrized ({{LIFEOS_DIR}} / __LIFEOS_DIR__ and the
config-dir equivalents) and both roots exported in EnvironmentVariables —
without that the daemons re-derive the wrong root at boot even with the code
fixed
- 14 installer substitution sites and the PULSE/MenuBar shell materializers
taught the new placeholders
- shell scripts (install.sh, StatusLine, Pulse manage scripts, Interceptor
helpers) resolve through CLAUDE_CONFIG_DIR
Behavior is unchanged on a default install: with both env vars unset every path
resolves exactly as before. pulse-old.ts is left alone as dead code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
LifeOS assumes it lives at
$HOME/.claude. The harness does not — it exposesCLAUDE_CONFIG_DIRprecisely so the config dir can live elsewhere. Anyone who uses it(a second install kept off a
~/.claudethat an employer syncs, a multi-profile setup,a non-default layout) gets ~165 call sites resolving
join(homedir(), ".claude", …)against a directory that is not their install.
Two failure classes, and the first is the nasty one:
1. Silent wrong-tree reads.
settings.json,skills/,hooks/,agents/,projects/andCLAUDE.mdexist in both trees. The lookup succeeds — against thewrong file. Nothing throws. Observed: the Pulse wiki and freshness tabs inventory a
different install's skills and hooks;
GetCounts,GenerateTelosSummaryandobservabilityread asettings.jsonthat is not the live one;AgentInvocationreadsagent frontmatter from an unrelated
agents/; memory harvesting walks the wrongprojects/transcripts.2. Install-time baked paths. The
Install*.tstools materialize launchd plists withliteral
$HOME/.claude/...program args and log paths, and never exportLIFEOS_DIRorCLAUDE_CONFIG_DIRinEnvironmentVariables. So even with the code fixed a daemonre-derives the wrong root at boot, because the environment it actually runs under is
empty. Worth stressing: fixing the code alone was not enough downstream — the plists
had to carry the env too.
The codebase already knows the right pattern; it is just applied unevenly. These are
correct today:
while the shared resolver every hook imports is not.
Fix
1.
hooks/lib/paths.ts— the one that matters.getClaudeDir()honoredCLAUDE_PLUGIN_ROOTbut notCLAUDE_CONFIG_DIR, sogetSettingsPath(),getSkillsDir(),getHooksDir()andgetEnvPath()pointed at~/.clauderegardless.One branch fixes every consumer that goes through the resolver.
export function getClaudeDir(): string { const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT; if (pluginRoot) { return expandPath(pluginRoot); } + const configDir = process.env.CLAUDE_CONFIG_DIR; + + if (configDir) { + return expandPath(configDir); + } + return join(homedir(), '.claude'); }and
getLifeosDir()'s last resort follows it instead of re-hardcoding:2. 167 direct call sites across
LIFEOS/**andhooks/**that bypass the resolver,guarded mechanically:
Collapsing these into
paiPath()/getClaudeDir()imports would be nicer; the inlineguard is the low-risk first step and keeps the diff reviewable line by line. Happy to
follow up with the import version if you prefer it.
3. Installers and templates. 16 plist templates parametrized (
{{LIFEOS_DIR}}/__LIFEOS_DIR__and the config-dir equivalents), both roots exported inEnvironmentVariables, and the 14 substitution sites plus the Pulse/MenuBar shellmaterializers taught the new placeholders. Shell scripts (
install.sh, StatusLine, thePulse manage scripts, Interceptor helpers) resolve through
CLAUDE_CONFIG_DIR.Compatibility
Both env vars unset →
getClaudeDir()returns~/.claudeand every rewritten siteresolves exactly as before. Byte-identical on a default install, no migration.
CLAUDE_PLUGIN_ROOTstill wins overCLAUDE_CONFIG_DIR, so plugin installs areuntouched.
Verified:
getClaudeDir()getLifeosDir()~/.claude~/.claude/LIFEOSCLAUDE_CONFIG_DIR=/tmp/alt/tmp/alt/tmp/alt/LIFEOS+ CLAUDE_PLUGIN_ROOT=/tmp/plug/tmp/plug/tmp/plug/LIFEOSChecks run
.tsfiles parse clean.shpassesbash -nplutil -lintpulse-old.ts(dead code, deliberately untouched)
Notes from running this downstream
Applied on a relocated install (config dir outside
~/.claude) before opening this PR:Pulse healthy after reload, and every runtime path in the daemon logs now resolves to
the real install root instead of the unrelated tree that happened to sit at
~/.claude.