[BUG] Desktop app: plan file links can never be opened — ~/.claude/plans is never added to the File Pane's allowed roots

Status Fixed / completed
Reported on v2.1.258
Maintainer reply None cached
Activity 0 comments · opened Sep 2, 2026 · closed Sep 4, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Summary

Plan mode writes plans to the global ~/.claude/plans/. The desktop File Pane will only render files contained in the session's working directory plus the two paths supplied via memory_paths (auto, team). Nothing ever adds ~/.claude/plans to that set, so every plan file link in the desktop app fails to open, for every user, on every project, 100% of the time.

The failure is not a missing file and not an OS permission problem. The read is refused client-side, before any filesystem call, and the user is shown a message that misattributes it to deletion.

The app writes these files itself, and reads other files from the same ~/.claude directory (settings.json, memory, transcripts) on every launch through the main process. Only the File Pane's display path applies this restriction.

Environment

| | |
|---|---|
| Claude desktop app | 1.44121.1 |
| Claude Code CLI | 2.1.258 |
| OS | macOS 26.6.2, arm64 |
| Session type | local (not remote/cloud, not SSH, not Remote Control) |
| Relevant settings | none — no permissions.additionalDirectories set at user or project level |

Reproduction

  1. Open any project in the desktop app (working directory e.g. ~/Dev/myproject).
  2. Enter plan mode and let Claude produce a plan. The plan is written to ~/.claude/plans/<slug>.md.
  3. The plan renders correctly in the conversation.
  4. Click the plan's file link / header to open the file itself.

Expected: the File Pane renders ~/.claude/plans/<slug>.md.

Actual: the File Pane shows

Couldn't find this file It may have been deleted, or it lives outside the working directory. /Users/<user>/.claude/plans/<slug>.md

The file exists and is readable. In my case: -rw-r--r-- 1 feras staff 21354 Sep 2 12:16 given-all-the-findings-dynamic-robin.md, written by the app minutes earlier, opening fine in any other editor.

Root cause

Traced through the shipped renderer bundles. Minified identifiers below are build-specific and will differ across releases; the control flow is the stable part.

1. The read gate — ion-dist/assets/v1/c5da7eb42--bHbCpLJ.js

Before fetching bytes for the pane, the session handle is resolved to { root, extraRoots, uncontained } and the requested path is containment-checked:

} else {
  let e = su(i.root, t, i.extraRoots);
  if (!e) {
    lu(!t || t.includes("\0") || t.startsWith("~") ? "malformed_path"
       : i.root?.startsWith("/") ? (i.extraRoots.length === 0 ? "no_mount_seed" : "outside_roots")
       : "no_cwd_seed");
    return r("client_denied");
  }
  a = e;
}

lu() emits telemetry claudeai.ccr.epitaxy_read_denied. For this case the reason is outside_roots. No filesystem call is made — the file is never stat'd.

The containment test is lexical (p === root || p.startsWith(root + "/"), after a string-only ./.. normalizer). No realpath, no symlink resolution.

2. What populates the allowed roots

root is written from two places only:

$c.set(e, n.cwd)          // from the CLI's system/init message
$c.set(e, r.workingDir)   // from a session record

extraRoots lives in one map whose sole writer is Cl(sessionId, path). Exhaustive enumeration of every call site in the bundle:

Cl(e, r?.auto)   // r = n.memory_paths, from the CLI's system/init message
Cl(e, r?.team)   // ditto
Cl(e, t)  x2     // for (let t of n.active_mount_paths ?? []) — remote session API record only

The active_mount_paths branch reads off the cloud session payload (the same object carrying environment_kind, self_hosted_runner_pool_id). It is never populated for a local desktop session.

So for a local session the complete set of renderable roots is: cwd + memory_paths.auto + memory_paths.team. ~/.claude/plans is not among them and there is no code path that could add it.

3. Error classification — ion-dist/assets/v1/cd5a31703-DPCARDPv.js

client_denied yields a null read classified missing. That is then combined with the (separately computed) outsideRoot flag:

function zE({outsideRootUnrenderable: e, outsideRoot: t = false, nullReadClass: n, isBridge: r}) {
  return e ? "outside_root"
       : n === "missing" ? (t ? "missing_or_outside" : "missing")
       : ...
}

producing message id IcjhOvUX8s (ion-dist/i18n/en-US.json): "It may have been deleted, or it lives outside the working directory."

Note there is already a precise, honest string for this condition — l5hJ1Ge03E, "This file is outside the working directory" — reached only via outsideRootUnrenderable. The plan case takes the hedged branch instead and speculates about deletion for a file it never looked at.

Why this has gone largely unreported

ExitPlanMode carries the plan's full text inline as plan, alongside planFilePath:

if (t.name === "ExitPlanMode") {
  let t = VD(n.plan);
  if (t !== void 0) { ... e.content = t }          // rendered from memory
  let r = VD(n.planFilePath);
  if (r !== void 0 && HD(r)) e.path = r            // link target only
}

The plan therefore displays correctly at creation time without the filesystem being touched. Only clicking through to open the plan as a file — i.e. revisiting a plan later — hits the resolver. Combined with the fact that virtually every other file link the app emits points inside the working directory, the broken surface is narrow enough to read as an isolated glitch rather than a systematic break.

Impact

  • Plans accumulate in a directory the app that wrote them cannot display. I have 24 spanning several months, none openable from the UI.
  • The error message actively misleads: users conclude the file was deleted or that their permissions are wrong. macOS Full Disk Access does not help and cannot, since no syscall is attempted.
  • There is no configuration that fixes this. permissions.additionalDirectories looks like the lever and is not: it feeds sessionFolders and executeRoot (display metadata, and the wording of this very error) but is never wired into extraRoots. /add-dir does not exist in the desktop app.

Suggested fix

Preferred, ~1 line: declare the plans directory alongside the memory paths in the CLI's system/init message and add the corresponding Cl(e, r?.plans). The mechanism already exists and is already used for two other ~/.claude subdirectories; plans were simply never added to it.

Alternative: have permissions.additionalDirectories actually feed extraRoots, so the setting that appears to control this does. Worth doing regardless — the current behaviour is a trap.

Independently, fix the message. When the denial reason is outside_roots, use the existing l5hJ1Ge03E string rather than speculating about deletion. Surfacing the reason code would save the next person this trace.

Secondary bug found in the same function

Any path beginning with ~ is rejected as malformed_path before containment is evaluated. A tilde-form link is therefore unopenable even when its target is inside an allowed root. Not what bit me — the emitted path is fully expanded — but it will defeat the fix above wherever a path is rendered in tilde form.

Related issues

  • #61443 — same resolver, "outside the session folder" on existing files (Windows); closed as duplicate, so a parent issue exists
  • #51331 — same class, SSH sessions
  • #45728, #14186 — plan mode writes to ~/.claude/plans ignoring project-level overrides
  • #21042, #12707 — requests to relocate plans into the project; both closed as not planned, which makes the declaration side (above) the right place to fix this

What Should Happen?

Open plans without permissions issues

Error Messages/Logs

Steps to Reproduce

Create a plan, deny it ask to revise in auto mode and then click on the link with the updated lines. Permissions denied in the open file pane

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.44121.1 (0ec430) 2026-09-02T02:45:14.000Z

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗