fix: honour CLAUDE_CONFIG_DIR — hooks and always-on tools work on non-default config roots#1429
Open
borfast wants to merge 6 commits into
Open
fix: honour CLAUDE_CONFIG_DIR — hooks and always-on tools work on non-default config roots#1429borfast wants to merge 6 commits into
borfast wants to merge 6 commits into
Conversation
Claude Code supports multiple profiles via CLAUDE_CONFIG_DIR, but the
hooks payload assumed the config root is always ~/.claude, at three
levels:
- hooks.json ships every command as $HOME/.claude/hooks/... and
InstallHooks.ts merged them verbatim, so on a non-default profile all
60 hook entries pointed at files the installer never deployed there —
every hook event errored.
- hooks/lib/paths.ts getClaudeDir() only honored CLAUDE_PLUGIN_ROOT,
never CLAUDE_CONFIG_DIR.
- ~20 hook files bypassed lib/paths.ts with their own inline
join(homedir(), '.claude', ...) spellings.
Fixes, in the spirit of the agentic installer (DetectEnv.ts already
detects CLAUDE_CONFIG_DIR correctly — the rest of the pipeline just
ignored what it found):
- InstallEngine.rewriteHooksConfigRoot(): retargets the default-root
spellings ($HOME/${HOME}/~ + /.claude) in payload hook commands at
the detected configRoot at merge time. No-op when configRoot IS
~/.claude, so default installs stay byte-identical.
- getClaudeDir() falls back CLAUDE_PLUGIN_ROOT → CLAUDE_CONFIG_DIR →
~/.claude; getLifeosDir() default now derives from getClaudeDir().
- All hook files with inline .claude paths now route through the
lib/paths.ts helpers (getClaudeDir/getSkillsDir/getSettingsPath/
paiPath).
- install.sh skills-dir detection probes CLAUDE_CONFIG_DIR before the
~/.claude and ~/.config/claude defaults.
- Nested skill-payload mirrors (install/skills/LifeOS/...) synced.
Verified: InstallHooks.ts --apply against a sandbox config root rewrites
all 60 commands (0 left on the default root), preserves pre-existing
foreign hook entries, and the rewrite is a no-op for a default-root
install.
Companion to the hooks fix: the tools that hooks invoke on every turn (MemoryHealthCheck, FreshnessCache, SettingsBackport, MemoryReviewer, ISARender) and the shared LifeosConfig loader still hardcoded ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile they read from — and wrote observability/state files into — the wrong profile. Adds LIFEOS/TOOLS/Paths.ts mirroring hooks/lib/paths.ts resolution (CLAUDE_PLUGIN_ROOT → CLAUDE_CONFIG_DIR → ~/.claude; LIFEOS_DIR override for the data dir) and routes those tools' root constants through it. Default installs resolve to the same paths as before.
…orts Second sweep after runtime verification against a non-default CLAUDE_CONFIG_DIR profile caught stragglers the first pass missed: - 13 hooks + handlers/UpdateCounts.ts still resolved LIFEOS_DIR or the Claude root with inline 'process.env.LIFEOS_DIR || join(HOME, ".claude", ...)' / 'resolve(homedir(), ".claude")' fallbacks — they wrote MEMORY state/observability files into ~/.claude from a ~/.claude-gc profile. All now route through lib/paths.ts. - lib/safety-classifier.ts TRUSTED_PREFIXES trusted the literal ~/.claude instead of the active config root. - Five LIFEOS/TOOLS files (lifeos.ts launcher, algorithm.ts, TranscriptParser.ts, IntegrityMaintenance.ts, SessionHarvester.ts) imported hooks/lib via '../../../.claude/hooks/lib/...' — a relative path that climbs OUT of the config root and back in through a literal '.claude' segment. Now the structure-relative '../../hooks/lib/...', which resolves in any profile. Verified: all 46 wired hooks execute from a ~/.claude-gc profile with zero module-resolution or path failures, write state/observability only under the active profile, and leave ~/.claude untouched. Display-only strings that mention ~/.claude in user-facing messages are left as-is.
The launcher hardcoded CLAUDE_DIR/BANNER_SCRIPT at ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile it would cd into and read MCP state from the wrong profile even though the spawned claude process inherits the right env. WALLPAPER_DIR stays home-relative by design.
The always-on hooks' guidance messages (memory-health 'Run: bun ~/.claude/...', writing-gate PangramScore instructions, Art workflow listing, system-file-guard import suggestion, work-repo setup hint) hardcoded ~/.claude, so on a non-default CLAUDE_CONFIG_DIR profile they told the user to run commands against the wrong profile — commands that either fail or, worse, act on another install. Adds displayPath() to hooks/lib/paths.ts (abbreviates the home prefix to ~ for display) and builds those messages from the already-resolved path constants. Default-root installs render byte-identical messages.
The message-correctness fix is building hints from resolved path constants; displayPath() only abbreviates the home prefix so default installs render byte-identical strings.
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
Claude Code supports multiple profiles via
CLAUDE_CONFIG_DIR, and the agentic installer already detects it correctly (DetectEnv.tsreports the rightconfigRoot) — but the rest of the pipeline ignores what it found. Installing v6.0.5 into a profile at~/.claude-gcproduced:settings.jsonpointing at$HOME/.claude/hooks/...— a location where this installer never deployed anything, so every hook event errored on the next session.~/.claude, reading the wrong profile's settings and writing MEMORY state/observability files into a different profile than the one that loaded them.This is the same class of issue as #830/#284, but scoped to the new agentic-installer ship model rather than the old full-clone one — per the direction stated there: "The installer hands integration to your own AI, which reads your actual machine (your OS, your paths, your harness) and wires the hooks." This PR makes the shipped tooling deliver that for non-default config roots.
Changes
InstallEngine.rewriteHooksConfigRoot(): at merge time,InstallHooks.tsretargets the default-root spellings ($HOME/${HOME}/~+/.claude) in payload hook commands at the detectedconfigRoot. No-op when the config root is~/.claude, so default installs stay byte-identical.hooks/lib/paths.tsgetClaudeDir()now falls backCLAUDE_PLUGIN_ROOT→CLAUDE_CONFIG_DIR→~/.claude(hooks inherit the env var from the harness that loaded them). NewLIFEOS/TOOLS/Paths.tsmirrors this for runtime tools, andLifeosConfig.tsdefaults derive from it.join(homedir(), '.claude', ...)-style fallbacks now route through the shared helpers. FiveLIFEOS/TOOLSfiles (including thelifeos.tslauncher) imported hooks libs via../../../.claude/hooks/lib/...— a relative path that climbs out of the config root and back in through a literal.claudesegment; now the structure-relative../../hooks/lib/..., which resolves in any profile.displayPath()inhooks/lib/paths.ts; the always-on hooks' guidance messages (memory-health "Run: bun …", writing-gate PangramScore instructions, Art workflow listing, etc.) are built from the resolved path constants instead of a hardcoded~/.claude, so users are never told to run a command against the wrong profile. Default-root installs render byte-identical messages.install.sh— skills-dir detection probesCLAUDE_CONFIG_DIRbefore the~/.claude/~/.config/claudedefaults.install/skills/LifeOS/...) kept in sync.Verification
Performed a real install into a live
CLAUDE_CONFIG_DIR=~/.claude-gcprofile (Linux, Claude Code):InstallHooks.ts --apply: all 60 commands rewritten to the profile root (0 left on the default), pre-existing foreign hook entries preserved, backup created.~/.claudeleft untouched.bun buildclean on all touched files).Out of scope
LIFEOS/TOOLSfiles not invoked by hooks (plus Pulse and skill tools) are swept in the follow-up PR fix: sweep remaining ~/.claude hardcodes across LIFEOS/TOOLS, PULSE, and skills (follow-up to #1429) #1431, which is stacked on this branch. This PR stays scoped to the runtime-verified always-on surface.