Skip to content

Public installs: derivedsync launchd job permanently exits 1 — DeriveDenyHashes.ts writes to the stripped private skills/_LIFEOS/ path (which is also the dev-tree marker) #1488

Description

@bvdnu

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions