Skip to content

SystemFileGuard and the ISA registry assume a single checkout: desktop-app worktrees under .claude/worktrees/ break both #2396

Open

Description

@jacobo-ortiz

What happens

The Claude Code desktop app can open a session in a git worktree of ~/.claude, at ~/.claude/.claude/worktrees/<name>/ (a full checkout of the same repo). Two parts of LifeOS reason about paths relative to ~/.claude and read those copies as foreign paths:

  1. hooks/lib/system-file-guard-core.ts → classifyTarget. It computes the path relative to ~/.claude and matches it against the containment zones. .claude/worktrees/<name>/LIFEOS/MEMORY/WORK/<slug>/ISA.md matches no zone, so it classifies as SYSTEM, while the same file in the live tree classifies as USER. Result: in a worktree session the guard blocks writing an ISA, memory or a private skill file that contains the principal's identity. On one install, 7 of 15 historical guard blocks were exactly this.
  2. hooks/lib/isa-utils.ts → syncToWorkJson. relativeIsa = isaPath.replace(paiDir + '/', '') only strips the live-tree prefix, so an ISA written inside a worktree is registered in work.json with its absolute worktree path. The row dangles once the worktree is removed, and it splits from the copy that SessionCleanup edits by slug in the live tree (the row and the live ISA can disagree on phase and progress).
  3. LIFEOS/TOOLS/WorkReconcile.ts. join(paiDir, session.isa) with that absolute path produces a path that does not exist (<LIFEOS_DIR>/<the absolute path again>), so the row is never reconciled.

Repro

import { classifyTarget } from "./hooks/lib/system-file-guard-core";
const root = `${process.env.HOME}/.claude`;
const rel = "LIFEOS/MEMORY/WORK/x/ISA.md";
classifyTarget(`${root}/${rel}`).classification;                         // "user"
classifyTarget(`${root}/.claude/worktrees/w/${rel}`).classification;     // "system"

Verified on v7.40.4 and on main at 5e2f2e8: the three lines above are unchanged and none of those files mentions worktrees.

Suggested fix

One small helper that maps <root>/.claude/worktrees/<name>/<rest> to <root>/<rest> (and returns any other path unchanged), used in three places:

  • classifyTarget: classify the mapped path, so a worktree copy gets the same class as its live twin (SYSTEM files in the worktree stay SYSTEM and keep being scanned).
  • syncToWorkJson: compute relativeIsa from the mapped path, so the row is stored relative to LIFEOS_DIR.
  • WorkReconcile: resolve a stored worktree path to its live twin before reading it.

We have run this locally since 2026-10-25 with probes that fail without it and pass with it.

Related

#1579 (closed) touched worktrees only in the --local launcher, the opposite direction. Nothing else found for "worktree" or "SystemFileGuard path classification".

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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