Support project-scoped installs (honor CLAUDE_CONFIG_DIR/LIFEOS_DIR everywhere) - #1666
Open
rogdex24 wants to merge 12 commits into
Open
Support project-scoped installs (honor CLAUDE_CONFIG_DIR/LIFEOS_DIR everywhere)#1666rogdex24 wants to merge 12 commits into
rogdex24 wants to merge 12 commits into
Conversation
… everywhere) LifeOS's install Tools already accepted --config-root/--config-dir, but ~60 files across the install payload hardcoded $HOME/.claude (or homedir()) directly, bypassing those flags. A project-scoped install (e.g. --config-root <project>/.claude, so LifeOS doesn't leak into every Claude Code session) would silently read/write the real global ~/.claude instead once past the install step — hooks pointing at files that were never placed there, memory/knowledge tools writing to the wrong tree, etc. - InstallHooks.ts: hooks.json ships commands hardcoded to "$HOME/.claude/..."; rewrite that prefix to the actual --config-root before merging into settings.json. - InstallSettings.ts: env template values under $HOME/.claude now expand to --config-root instead of unconditionally expanding to the real home dir. - install/hooks/lib/paths.ts: getClaudeDir() now honors CLAUDE_CONFIG_DIR (it already honored CLAUDE_PLUGIN_ROOT for packed-plugin installs) — this is the central resolver several core hooks (PromptProcessing, EventLogger, LoadContext, AgentInvocation) depend on. - ~55 further files across LIFEOS/TOOLS, PULSE, hooks/, and skills/ Tools: added the same CLAUDE_CONFIG_DIR / LIFEOS_DIR fallback ahead of each hardcoded homedir()+".claude" path, matching the convention already used correctly in Conveyor/Ledger.ts and PULSE/modules/content.ts. Every change is additive (env var checked first, falls through to the exact original default) — zero behavior change for a normal global install with these vars unset. - ContextAudit.ts: its cross-reference validator only recognized ~/.claude/LIFEOS/... as a valid absolute prefix, so it would flag every correct project-scoped doc reference as broken; added the missing branches. - Docs: INSTALL.md and Workflows/Setup.md now document the existing CLAUDE_CONFIG_DIR/LIFEOS_DIR mechanism as the supported way to scope an install to one project, since previously nothing surfaced that this was possible. Left untouched (confirmed cosmetic, no functional path): usage/doc strings, console.log/error message text, and a couple of files already correctly guarded by an existing env-var check. 🤖 Generated with Claude Code Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Current Claude Code versions only match Edit(path) rules for file-editing tools — a bare Write(pattern) allow/ask/deny rule is a silent no-op, and the harness prints a startup warning for each one telling you so. Every Write(...) rule in settings.system.json already had a matching Edit(...) rule covering the same path (confirmed before removing), so this is pure dead-weight cleanup — the actual protections were never affected, only the noise. Applies to both copies (the top-level template and the nested one bundled inside the LifeOS skill's own install/ payload, kept in sync).
The README's install section had no pointer to the CLAUDE_CONFIG_DIR/LIFEOS_DIR mechanism documented in INSTALL.md and Workflows/Setup.md — someone skimming just the README would never know project-scoped installs are possible. Added a one-line callout with a link, and a proper heading in INSTALL.md for the anchor to resolve to.
Both launch paths (cmdLaunch without --local, and cmdPrompt unconditionally) called process.chdir(CLAUDE_DIR). For the legacy global default (~/.claude) this is harmless — but for a project-scoped install (CLAUDE_CONFIG_DIR set to <project>/.claude), it chdir's INTO the .claude directory itself, which breaks the harness's own project-root detection (it needs cwd at <project>/, with .claude/ as a subdirectory, not cwd already inside .claude/). Fix: only auto-chdir when CLAUDE_DIR is still the default global path. A project-scoped install now keeps whatever directory the caller invoked from — exactly what --local already did for the global case, made automatic so a project-scoped launch alias doesn't need to remember the flag. Found while wiring the `lifeos` launch alias for a real project-scoped install: it needed --local plus an explicit `cd <project>` to work at all before this fix.
InstallHooks.ts was unconditionally rewriting hooks.json's literal
"$HOME/.claude/..." commands to the resolved configRoot, even for a default
(unset CLAUDE_CONFIG_DIR) global install. That's a real behavior change for
the default path: the literal "$HOME/..." form is intentionally portable
(shell-expanded at hook-execution time), so baking in a fully-resolved
absolute path breaks the case of syncing settings.json across machines or
user accounts — something the default install never asked for and never did
before this PR. InstallSettings.ts already got this right (only rewrites
when configRoot differs from the default); InstallHooks.ts now matches it.
Also simplified ContextAudit.ts's normalizeReference: the LIFEOS_DIR-rooted
check already covers the plain-HOME case (LIFEOS_DIR defaults to exactly
${HOME}/.claude/LIFEOS when unset), so the separate literal-HOME branch was
dead weight in the default path — removed, one check now covers both.
Found via a final review pass across all changed files specifically checking
for default/global-install regressions.
Found via a real project-scoped install + /interview run: CLAUDE.md's routing table said "paths are relative to ~/.claude/ unless noted" as flat fact. The DA read that literally and ran `bun ~/.claude/LIFEOS/TOOLS/InterviewScan.ts` against the global path — "Module not found" — even though the tool exists correctly at the project-scoped root and every install Tool already resolves through CLAUDE_CONFIG_DIR/LIFEOS_DIR. This is a different class of bug than the earlier path-resolution sweep: that fixed hardcoded paths in *code*; this is a hardcoded assumption in the one *doc* that's always loaded and read by the AI as ground truth. Grepping further turned up the same "bun ~/.claude/..." example-command pattern in 56 more docs across LIFEOS/DOCUMENTATION and skill Workflows/SKILL.md files — too many to chase individually, and it'll keep recurring as skills ship new docs. Fixed the root of it instead: CLAUDE.md now tells the AI explicitly that any literal ~/.claude/... in ANY doc is illustrative, not a fact about the current install, and to resolve CLAUDE_CONFIG_DIR/LIFEOS_DIR before running anything literally.
…issed
The original sweep (PR's first commit) matched homedir()+'.claude' and literal
$HOME/.claude / ~/.claude, but missed three more syntactic variants actually
used throughout the codebase:
- join(HOME, ".claude", ...) / resolve(HOME, ".claude", ...) — a pre-resolved
local `const HOME = process.env.HOME || ""` passed as a plain identifier,
not a homedir() call.
- join(HOME, ".claude/LIFEOS/...") — a single path string with more after
".claude" rather than a lone ".claude" argument.
- join(process.env.HOME ?? "~", ...) / `${process.env.HOME}/.claude` —
inline env access instead of a named HOME constant.
Found this because it was live: a /interview session hit "Module not found"
running InterviewScan.ts against the wrong (global) path, and separately
credential/.env lookups in several PULSE/healthsync tools were silently
checking ~/.claude/.env instead of the project-scoped one.
84 files fixed, same additive pattern as the original sweep throughout
(CLAUDE_CONFIG_DIR for general Claude-home paths, LIFEOS_DIR for the
LIFEOS-specific subtree), verified via `bun build --no-bundle` syntax checks
and three successive convergence passes with progressively broader regexes
until a pass turned up zero new genuine hits (all remaining matches confirmed
cosmetic — doc strings, error/usage text, or the two files whose matches are
themselves regex *patterns* for detecting this exact bug, not the bug).
This is very likely still not 100% exhaustive for a codebase this size and
with markdown docs the AI reads as instructions (a related, separate class of
issue fixed in the prior commit for CLAUDE.md) — flagging that residual risk
rather than claiming otherwise.
…ion started from Found this exact line early on but it got lost in the broader sweep instead of being fixed directly — this is the actual root cause of the "Module not found" error reported live during a /interview session (InterviewScan.ts itself resolved fine via LIFEOS_DIR at line 38; only this one credential-check path at line 101 was still hardcoded to ~/.claude/.env).
….claude CLAUDE.md now carries a blanket rule that any ~/.claude/... in a doc's examples is illustrative, not fact — but that only helps if the reader (AI or human) has CLAUDE.md loaded in context. Someone opening a skill's SKILL.md or a DOCUMENTATION/*.md file directly, without that context, would still hit the same "ran the literal global path, got Module not found" bug we found live. Added the same one-line reminder directly to each of the 56 affected docs, right after their title, so every doc is self-contained on this point. No other content touched — frontmatter, existing example commands, and body text are unchanged; this is purely an additive callout.
1. All 10 InstallXXX.ts scripts (BookmarkSweep, CommitmentSweep, WorkSweep,
DerivedSync, BlogDiscovery, CodexUpdate, Conveyor Runner/Watcher,
UsageAggregator, HealthSync) substitute {{HOME}}/__HOME__ placeholders in
their .plist.template into a real launchd job — but always with the
literal real $HOME, never LIFEOS_DIR/CLAUDE_CONFIG_DIR. Each template
hardcodes "{{HOME}}/.claude/LIFEOS/..." as a compound unit, so a project-
scoped install's background services (these are optional launchd jobs)
would always target the global path regardless of --config-root.
Fixed with a two-pass substitution: first replace the compound
"{{HOME}}/.claude/LIFEOS" (or "__HOME__/.claude/LIFEOS") prefix with the
resolved LIFEOS_DIR, THEN do the normal bare {{HOME}}/__HOME__ replace for
whatever's left (PATH entries, the plist's own HOME env key — those are
legitimately meant to be the real home, not the project dir). Verified the
default (LIFEOS_DIR unset) case produces byte-identical output to before.
2. PULSE.toml's "memory-consolidation" cron job (daily 3am, type="claude")
has a `prompt` field fed directly to an AI, literally instructing it to
run `bun run ~/.claude/LIFEOS/TOOLS/SessionHarvester.ts` — the same doc-
instruction bug class already fixed in CLAUDE.md and 56 markdown docs,
just living in a .toml config instead. Reworded the prompt to resolve
LIFEOS_DIR/CLAUDE_CONFIG_DIR first instead of assuming the global path.
Checked settings.enhancements.json too — its two ~/.claude mentions are
cosmetic UI tooltip text, not functional paths, confirmed no fix needed.
Found live: pulse.ts failed with "Cannot find package 'smol-toml'" style
errors that traced back to it hardcoding LIFEOS_DIR — a file our prior
sweeps should have caught but didn't. Root cause: the classifier used to spot
these bugs checked whether the string "LIFEOS_DIR" (or similar) appeared
nearby a hardcoded line as proof of being "guarded" — but a line like
`const LIFEOS_DIR = join(HOME, ".claude", "LIFEOS")` trivially contains that
substring because it's THE NAME OF THE VARIABLE BEING DEFINED, not an actual
env-var check. Any file with a local const named LIFEOS_DIR/CLAUDE_ROOT/etc.
assigned a hardcoded value slipped through every previous sweep undetected.
Rewrote the check to require an actual `process.env.X` read (or equivalent
bash `${X:-...}`) to count as guarded. Re-ran across the full tree and found
22 more files with genuine unguarded hardcodes — several in files we'd
already partially fixed, where a SECOND, separate definition in the same
file was missed the first time (e.g. GetCounts.ts, CostTracker.ts, setup.ts,
observability.ts, tab-freshness.ts, telegram.ts, ContextSearch.ts all had
this). Two more (GrokAudit.ts:112, telegram.ts:490/517) weren't even in the
classifier's output — found by having agents read whole files instead of
trusting single flagged lines.
Also includes pulse.ts itself (fixed live during triage, synced here) and
its sibling pulse-unified.ts (same bug, same fix).
Re-ran the corrected classifier after all fixes: converged to exactly the
same known-cosmetic set (doc/error/usage text, and the handful of files
whose matches are themselves pattern-matching logic, not the bug) — no new
genuine hits.
…hboard text
A different category from prior fixes: this is client-side React text
("use client" components), rendered directly in the live Pulse dashboard a
human reads — not code that resolves paths at runtime. A project-scoped
user reported seeing "~/.claude/LIFEOS/USER/HEALTH/" in the dashboard's
empty-state guide, which is wrong for their install.
No config API exists for the client to fetch its real resolved root, and
building one just for display text would be disproportionate to the actual
problem (nothing reads/writes the wrong file — it's purely informational).
Fixed by dropping the presumed-absolute "~/.claude/" prefix from ~20 UI
strings across 10 files (both descriptive text and copy-pasteable command
snippets like "bun ~/.claude/LIFEOS/TOOLS/CostTracker.ts status", which
would literally fail if copy-pasted by a project-scoped user) — displaying
"LIFEOS/..." instead, which is accurate regardless of install location.
Left two files untouched: a source comment in lib/effort.ts and three source
comments in app/agents/page.tsx — neither is ever rendered in the UI.
Note: this app builds to a static export (out/) that Pulse serves directly;
these source changes need `next build` re-run to actually reach a running
dashboard.
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.
Fixes #1665.
LifeOS's install Tools already accepted
--config-root/--config-dir, but ~60 files across the install payload hardcoded$HOME/.claude(orhomedir()) directly, bypassing those flags. A project-scoped install (e.g.--config-root <project>/.claude, so LifeOS doesn't leak into every Claude Code session) would silently read/write the real global~/.claudeinstead once past the install step — hooks pointing at files that were never placed there, memory/knowledge tools writing to the wrong tree, etc.Changes
InstallHooks.ts—hooks.jsonships commands hardcoded to"$HOME/.claude/..."; rewrite that prefix to the actual--config-rootbefore merging intosettings.json.InstallSettings.ts— env template values under$HOME/.claudenow expand to--config-rootinstead of unconditionally expanding to the real home dir.install/hooks/lib/paths.ts—getClaudeDir()now honorsCLAUDE_CONFIG_DIR(it already honoredCLAUDE_PLUGIN_ROOTfor packed-plugin installs). This is the central resolver several core hooks depend on (PromptProcessing,EventLogger,LoadContext,AgentInvocation).LIFEOS/TOOLS,PULSE,hooks/, andskills/*/Tools— added the sameCLAUDE_CONFIG_DIR/LIFEOS_DIRfallback ahead of each hardcodedhomedir()+".claude"path, matching the convention already used correctly inConveyor/Ledger.tsandPULSE/modules/content.ts. Every change is additive (env var checked first, falls through to the exact original default) — zero behavior change for a normal global install with these vars unset.ContextAudit.ts— its cross-reference validator only recognized~/.claude/LIFEOS/...as a valid absolute prefix, so it would flag every correct project-scoped doc reference as broken; added the missing branches.INSTALL.mdandWorkflows/Setup.mdnow document the existingCLAUDE_CONFIG_DIR/LIFEOS_DIRmechanism as the supported way to scope an install to one project, since nothing previously surfaced that this was possible.Left untouched (confirmed cosmetic, no functional path): usage/doc strings,
console.log/errormessage text, and a couple of files already correctly guarded by an existing env-var check.Test plan
bash -non all edited shell scripts — passbun build/transpile parse check on all edited TypeScript files — passgetClaudeDir()/getHooksDir()/getSkillsDir()/getSettingsPath()/getLifeosDir()and Conduit'sCLAUDE_ROOT/DATA_ROOTall resolve to the project path withCLAUDE_CONFIG_DIR+LIFEOS_DIRset||/${VAR:-default}fallback🤖 Generated with Claude Code