Summary
On a public install (fresh /lifeos-setup, no private _LIFEOS skill), the com.lifeos.derivedsync launchd job permanently exits 1: its deny-hashes action runs LIFEOS/TOOLS/DeriveDenyHashes.ts, which hardcodes its output to ~/.claude/skills/_LIFEOS/DENY_HASHES.json — a directory that only exists on the author's dev machine and is stripped at release.
Repro (any public install)
$ bun ~/.claude/LIFEOS/TOOLS/DeriveDenyHashes.ts
ENOENT: no such file or directory, open '~/.claude/skills/_LIFEOS/DENY_HASHES.json'
$ launchctl print gui/$UID/com.lifeos.derivedsync | grep "last exit"
last exit code = 1
Triggered on every run where the deny-corpus sources changed (identity, contacts, TELOS) — which is exactly when DerivedSync schedules the action, so the job keeps failing.
Why users can't work around it by creating the directory
skills/_LIFEOS doubles as the dev-tree marker: InstallEngine.detectDevTree() returns true when it exists, after which DeployCore/DeployComponents/LinkUser refuse to run. Creating the output dir to silence the error bricks future setup/update runs. So the tool ships in the public payload but can never succeed there, and the failure can't be safely worked around.
Note the consumer side already fails open: hooks/lib/system-file-guard-core.ts returns an empty list when DENY_HASHES.json is missing, so skipping the write on public installs loses nothing.
Suggested fix
Skip cleanly (exit 0) when the private skill is absent, after the --dry-run/--show-tokens handling so those local review modes keep working everywhere:
if (dryRun) { console.log("[DeriveDenyHashes] --dry-run: nothing written"); return; }
// Public installs don't ship the private _LIFEOS skill (its dir doubles as the
// dev-tree marker, so it must never be created here). Without it there is no
// home for the filter and no guard consuming it — skip the write cleanly.
if (!existsSync(join(CLAUDE, "skills", "_LIFEOS"))) {
console.log("[DeriveDenyHashes] skills/_LIFEOS absent (public install) — skipping hash write");
return;
}
const salt = loadSalt();
Verified locally on LifeOS 7.1.1 (macOS, arm64): manual run exits 0 with the skip message, com.lifeos.derivedsync last exit code is 0, dev-tree detection stays false, and behavior with skills/_LIFEOS present is unchanged (tested in a sandbox $HOME: salted hashes written exactly as before). Side benefit: the early return also avoids loadSalt() creating a stray ~/.claude/.env on installs that manage secrets elsewhere.
Summary
On a public install (fresh
/lifeos-setup, no private_LIFEOSskill), thecom.lifeos.derivedsynclaunchd job permanently exits 1: itsdeny-hashesaction runsLIFEOS/TOOLS/DeriveDenyHashes.ts, which hardcodes its output to~/.claude/skills/_LIFEOS/DENY_HASHES.json— a directory that only exists on the author's dev machine and is stripped at release.Repro (any public install)
Triggered on every run where the deny-corpus sources changed (identity, contacts, TELOS) — which is exactly when DerivedSync schedules the action, so the job keeps failing.
Why users can't work around it by creating the directory
skills/_LIFEOSdoubles as the dev-tree marker:InstallEngine.detectDevTree()returns true when it exists, after which DeployCore/DeployComponents/LinkUser refuse to run. Creating the output dir to silence the error bricks future setup/update runs. So the tool ships in the public payload but can never succeed there, and the failure can't be safely worked around.Note the consumer side already fails open:
hooks/lib/system-file-guard-core.tsreturns an empty list whenDENY_HASHES.jsonis missing, so skipping the write on public installs loses nothing.Suggested fix
Skip cleanly (exit 0) when the private skill is absent, after the
--dry-run/--show-tokenshandling so those local review modes keep working everywhere:Verified locally on LifeOS 7.1.1 (macOS, arm64): manual run exits 0 with the skip message,
com.lifeos.derivedsynclast exit code is 0, dev-tree detection stays false, and behavior withskills/_LIFEOSpresent is unchanged (tested in a sandbox$HOME: salted hashes written exactly as before). Side benefit: the early return also avoidsloadSalt()creating a stray~/.claude/.envon installs that manage secrets elsewhere.