Brennan Benson 51fe6f3fba fix(editor): restored tabs for files outside your projects no longer fail with Access denied (#24489)
* fix(editor): read files outside projects without a grant a restart loses

A file opened from outside every project (e.g. ~/notes.txt from the floating
workspace) read through an in-memory grant. After a restart the restored tab
only renewed that grant when it stored a full path, so a tab saved relative to
the floating workspace folder failed with "Access denied" and Retry repeated it.

Single-file reads (read, stat, exists) and open-editor-tab saves now resolve a
path outside every project in place. Paths inside a project keep the full
containment check, so a project's symlinks still cannot escape it, and every
other write stays inside projects.

* fix(editor): re-grant restored floating-workspace tabs by owner, not path shape

Problem: a file opened from the floating workspace (e.g. ~/notes.txt via
Cmd-click in the floating terminal, the floating markdown picker, or a .md
opened from the OS) loads until restart, then shows "Access denied: path
resolves outside allowed directories". Main's external-path grants live only
in memory. On restore the editor re-granted only tabs that stored an absolute
path, but floating tabs store a path relative to the floating root (~ by
default), which is deliberately not an authorized root, so they were never
re-granted. Restored floating notebooks also failed to start a kernel.

The previous commit on this branch let main read and save any path outside a
project without a grant. That widened fs:readFile/stat/pathExists for every
caller, including automatic reads of untrusted content (markdown preview
images), which opened a Windows UNC credential leak and a /dev/zero
main-process memory blowup. This reverts that model entirely.

Fix: one helper decides which client-local path a tab needs re-granted by
ownership: a floating-workspace tab, or a tab stored outside its own project.
It never grants paths a local project root covers (a grant would also
authorize a project symlink's outside target), and skips SSH-owned,
runtime-owned and not-yet-hydrated owners. Every reader that can touch a
restored tab before or without the editor loader uses it: the loader, the
restored dirty-tab conflict scan, and the paired-mobile markdown bridge.

* fix(editor): let main decide which restored-tab paths a project already covers

Problem: the restore re-grant helper decided "already inside a project" in
the renderer from its worktree list. At startup that list only holds repos
the session references, so a floating tab inside an unlisted repo was granted
(including a symlink's outside target), and the renderer's path matcher
disagrees with main's on WSL \\wsl$ vs \\wsl.localhost, which stranded a
folder-workspace tab with "Access denied" after restart. The helper also
treated a folder workspace with a missing or ambiguous host as local.

Fix: the renderer now decides only by owner (a floating-workspace tab, or a
tab stored outside a project whose owner is explicitly local) and asks main
with `skipIfInsideAllowedRoots`. Main checks the path against its own allowed
and registered roots, in both the named and canonical-parent spelling against
both root spellings: a path a project covers gets no grant, an alias spelling
of a project path gets only that spelling, and a project symlink's outside
target is never granted. Explicit-open grants (Cmd-click, drag, explorer) are
unchanged. Tests now prove each reader waits for the grant before reading.

* fix(fs): decide a restored tab's project membership from every ancestor's real path

Problem: the restore re-grant decided "inside a project" from the named path
and the real path of its parent only. When a tab path crossed a project
directory symlink and named the project through a spelling that was neither
the registered root nor its realpath (a second alias, a `..` segment, a case
variant on a case-insensitive disk, a /var-style alias of an ancestor), no
check matched, the path took the full grant, and the symlink's outside target
became readable and writable.

Fix: a path is inside a project when the named path is inside a root, or the
real path of any ancestor folder is inside a root in its registered or real
spelling. Such a path gets at most its named spelling, never its realpath. An
ancestor that fails to resolve for any reason other than "missing" now fails
closed to the named-spelling grant instead of falling through to the full one.
Tests cover each spelling; the non-symlink case also runs on Windows.

* fix(fs): read local files as regular files only, from one bounded handle

Problem: fs:readFile stat'ed a path and then read it to EOF. A character
device such as /dev/zero reports size 0, passes the size limit and never ends,
so the main process buffers until memory runs out; a FIFO hangs the open.
Writes could also target an existing device or FIFO.

Fix: every local fs:readFile (editor and log snapshot) opens the path once,
non-blocking, refuses anything but a regular file, and reads the size check,
binary probe and content from that same handle, capped at the limit even if
the file lies about its size. The AI Vault log tail opens non-blocking too,
and fs:writeFile refuses an existing non-regular target.

* feat(fs): let desktop file requests declare their shape

Problem: main decided every local file request against one allow-list plus a
set of in-memory grants the renderer had to recreate after every restart, so
a file the user opened outside a project (for example from the floating
workspace) was denied once Orca restarted.

This adds the request shape the common pattern uses, alongside the grants for
now:
- no shape (the default): the path must be inside a project root main
  recognises, symlinks included. Desktop requests also accept the app-owned
  floating-workspace folder; paired-client RPC never does.
- user-file: a single file the user named by absolute path, used in place.
  Only fs:readFile/stat/pathExists and saving (fs:writeFile) accept it.
- document-resource: an image or PDF a document references, limited to every
  project root when the document is in one, else to the document's folder,
  and refused by path text before any disk or network access.
Notebook kernels and AI Vault log tails check their open file as user-named.

* feat(editor): send each local file request's shape from the renderer

Problem: after a restart, a tab opened outside every project (a floating
workspace file, a file opened by absolute path, an OS-opened markdown) could
only be read if the renderer first re-granted its path, and readers that ran
before the editor loaded the tab had no grant at all.

The renderer now says what kind of request it is making, and main checks that:
- A persisted tab opened outside its owner's root (floating workspace, or an
  absolute stored path) whose owner is explicitly local reads and saves as a
  user-named file, from every reader: the editor loader, the restored-tab
  conflict scan, the change banner and compare dialog, the paired-phone
  markdown bridge and the save queue. Project tabs stay inside their root.
- Clicks, drops, typed paths and browser-opened notebooks stat as user-named.
- Markdown preview and rich-editor images are document resources, limited to
  the document's roots or folder. Images the user pasted or attached into a
  chat show as user-named; agent images stay inside the project.
- The image cache keys on the shape, so one shape's image never answers
  another's request.
A ratchet test lists every renderer file allowed to create a user-named
request.

* refactor(fs): delete the in-memory path grant system

Problem: main kept a set of paths the renderer had asked it to allow
(fs:authorizeExternalPath). The set lived only in memory, so a file the user
opened outside every project could be read until Orca restarted and was then
denied, and every new reader of a restored tab had to remember to recreate
the grant first. Three rounds of re-deriving grants at restore each found
another reader or path spelling it missed.

Now that every desktop request declares its shape, nothing needs a grant:
- delete the grant set, authorizeExternalPath, the restore re-grant from the
  earlier commits on this branch, the fs:authorizeExternalPath channel and its
  preload and web-client entries;
- delete every renderer grant call (terminal and markdown link clicks, drops,
  typed paths, the file explorer, AI Vault logs, chat attachments, browser
  notebooks) and every main one (floating markdown picker and folder, OS-opened
  markdown, keybindings.json, pasted images, import and upload sources);
- the floating workspace's picker-approved folders stay a terminal-cwd
  allowlist only.
Main now holds no per-path permission, so a restart can't change any answer.

* feat(editor): open project links that lead outside the project as named files

Problem: a file inside a project that is a symlink to something outside it
opened fine from the file explorer or a terminal Cmd-click, then showed
"Access denied" after a restart: its tab was stored as a project file, and a
project request is refused when it resolves out of the project. A folder link
out of the project expanded in the explorer until restart and then failed with
a raw access error.

Now the click decides and the tab keeps that decision. Both gestures stat the
path inside the project first; if only the user-named check passes, the path
leads out of the project:
- a file opens by its absolute path, so it reads and saves as a file the user
  named, the same before and after a restart;
- the explorer does not follow a folder link out of the project and says so
  ("This folder links outside the project, so it can't be opened here.").
Paths that stay inside the project still open as contained project tabs. Also
drops the AI Vault "path not authorized" message, which nothing shows now.

* chore: drop the casts the changed-code quality gate flags on this branch

The FileContent casts in the editor loader and the paired-phone markdown
bridge were never needed (the read result is already assignable). Tests stub
window.api through vi.stubGlobal and pass narrow stores without casting; the
one test store that still needs a cast states why.

* fix(fs): load chat images by type, and keep escaping project links readable

Problems found in review:
- Chat transcript images were trusted by message role: any user-role
  "[Image: source: <path>]" (an injected Claude record, `orca terminal send`,
  a paired client's image-ref) became an automatic user-named read as the row
  scrolled into view, of any file type, and on Windows a network-share path
  would have opened an SMB connection to that host.
- A document image named like an image but linking to a text file
  (logo.png -> .env) was read as text.
- Windows device names (NUL.png, COM1.jpg) passed the path-text check of the
  automatic image loads.
- A project symlink leading out of the project, opened by a typed path, a
  tab-strip drop or a browser file:// notebook, was stored as a project tab
  and immediately refused.

Fix:
- New chat-image request shape for every transcript image and the composer
  preview, whoever's turn named it: an absolute local path whose requested and
  real targets are image files, a regular file, size-capped; network-share and
  device-namespace paths and Windows device names are refused by path text
  before any filesystem call. Pasted screenshots still show after a restart,
  and agent images outside the project now render.
- Document resources check the real target's type too, and refuse Windows
  device names by path text.
- Typed paths, tab-strip drops and browser notebooks stat through the same
  check as the explorer and terminal, and open an escaping link by its
  absolute path.
- Tests pin the shape at the change banner, compare dialog, markdown preview
  and image prewarm; a second ratchet lists every file that can open a tab the
  tab rule reads as user-named, and its comment says what it can't see.
- Stale grant wording removed.

* fix(fs): tighten automatic image loads and the project-link check

Problems found in review:
- Two unit tests went red on this branch: the browser-share test still
  expected reads without a shape, and the rename test's electron mock had no
  app, which the desktop root check now needs.
- The device-name check ran on the raw path, so `NUL.png\.` or
  `COM1.png\x\..` (reachable from markdown `![](NUL.png%2F.)`) reached the
  filesystem; a document image whose real target was a device name passed.
- Chat images in a project that lives on a Windows network share no longer
  rendered, though the markdown preview showed them.
- Any failed project check (a missing file, a dropped connection) was taken
  as "this link leads out of the project" and opened as an absolute tab.
- Every local read allocated about 2 MiB, even for a tiny image.

Fix:
- Device names and device-namespace paths are checked on the resolved path
  and on the real target, for chat images and document resources alike.
- A network-share path in an automatic load is read only inside a project
  root (the user chose that share when adding the project); anywhere else it
  is still refused by path text before any filesystem call.
- Only main's "outside allowed directories" refusal marks a project path as
  leading out of the project; other errors surface as before. The message now
  lives in shared code so both sides agree on it.
- Reads size their first buffer from fstat and confirm EOF with a 1-byte
  probe; a file that grows past its reported size is still read in bounded
  chunks up to the cap.
- Fixed the two red tests.

* refactor(fs): name file access by its role, not its structure

Problem: the static-analysis anti-slop check failed the PR because the new
code named the request's file access a "shape" (`shape`, `RequestShape`,
`TabShape`), which describes structure rather than the role.

Rename the main-process module filesystem-request-shape.ts (and its tests) to
local-file-access-resolution.ts, rename the symbols to fileAccess,
FileAccessResolution and TabFileAccessFields, and say "file access" or
"access kind" in the comments and test names. No behaviour change.

* fix(fs): refuse every Windows device-name spelling in automatic image loads

Problem: the device-name check split a file name only on '.', so names such
as NUL:.png, COM1:.png, NUL:stream.png (an alternate data stream) slipped
through, and CONIN$, CONOUT$, CLOCK$, COM0 and LPT0 were not listed. Those
reached the filesystem from a document or chat image before being refused.

Split on ':' as well, list the missing device names, and test each with
Windows path rules and zero filesystem calls. Also cover the case of a local
link that leads onto a network share outside every project (refused for chat
images), and correct the shared comment on chat-image access.

* fix(editor): let users rename and insert images into files opened outside projects

Renaming a file opened outside every project (tab double-click, editor
header) and inserting an image into such a markdown document failed with
"Access denied", even before a restart: both writes only passed the
project-root check. Document resources and chat images were also limited
by file type more strictly than users expect.

- Add a "document-folder" access kind for writes beside a document the
  user opened: main allows renaming only that document, to a name inside
  its own folder, and importing new files only into that folder, checked
  by path text and again by real path, with Windows device names refused.
  The renderer sends it only for local user-named, writable tabs (rename,
  its undo/rollback, image insert); SSH and runtime requests never carry it.
- Document resources: drop the image/PDF type allowlist; folder
  confinement, regular-file reads, the cap and path-text refusals remain.
- Chat images: judge only the real target's type, against every
  previewable image type (AVIF added).

* fix(fs): a declared file-access kind never refuses what the project check allows

A full-path tab for a file inside a project (for example a link that
leads out, opened by its absolute path) was renamed under the
document-folder rule, which limited the new name to the file's own
folder, although the same rename with no declared access could move it
anywhere in the project. Any declared kind could be stricter than the
default in the same way.

Every desktop local file request now goes through one resolver,
resolveLocalRequestPath: it runs the default project check first (roots,
Orca's floating folder, symlink containment, outside-root path text
refused before any filesystem call) and only on a refusal applies the
declared kind's rule, which adds paths outside projects. Reads, saves,
rename source and target, and import destinations all use it, so a new
kind gets the rule for free. Automatic loads (document and chat) still
refuse Windows device paths and names by text first, even inside a
project; a device is never a file to show.

The document-resource rule no longer needs its own project branch, and
chat images no longer re-run the roots check for shares.

* fix(fs): symmetric outside-project renames, notebook real folder, same-share images

- Renaming a file opened outside every project accepted a name in a
  subfolder (`archive/todo.md`), but the Undo and the rollback rename,
  declared from the moved file, were then refused and the file stayed
  moved. A rename under document-folder access must now land directly in
  the document's own folder (checked by path text before any filesystem
  call), so rename, Undo and rollback are symmetric. Image import still
  accepts the folder or a folder under it. Inside projects the default
  check still allows any in-project target.
- A notebook opened through a link inside a project started its kernel in
  the link's folder instead of the real file's folder (main's behaviour),
  because notebook and AI Vault log-tail paths skipped the project check.
  Both now resolve through resolveLocalRequestPath (project check first,
  then the user-file rule).
- A markdown file opened from a Windows share outside every project could
  not show the images beside it. Document images on a share are now
  allowed inside the document's own folder; the folder text check refuses
  every other host and share before any filesystem call.
- resolveDesktopAuthorizedPath is async, so a synchronous failure in the
  default check rejects like any other refusal.

* fix(fs): refuse share images outside projects again; keep renames and kernels as on main

- Reverts the same-share document image rule from the previous commit.
  Its folder check compared hosts case-insensitively, so a host spelled
  with U+212A KELVIN SIGN (or a decomposed accent) passed as the
  document's own share and was contacted, reopening the network
  credential leak. Document and chat images on a share outside every
  project are again refused by path text before any filesystem call;
  tests now cover the look-alike hosts with zero filesystem calls.
- Renaming a file opened outside every project into a project folder
  passed the project check, but its Undo (declared from the new path)
  was refused and the file stayed moved. When the rename source is
  allowed only as the opened document, the new name must now land
  directly in the document's folder even if a project would accept it
  (resolveLocalRenamePaths).
- A notebook opened through a link outside every project started its
  kernel in the link's folder; main used the real file's folder. The
  kernel cwd is now the real file's folder in every case.

* fix(fs): a file opened outside every project renames to any path, and keeps its access

Renaming a document the user opened (floating workspace or full-path tab) now
follows the user-file rule: the source must be the opened document, and the
new path can be any absolute path, so a rename into another folder, a
subfolder or a project works, and its Undo (declared from the moved file)
comes back from there. Any other rename keeps the project check only. Remove
the same-folder rename rule and its tests; image import stays in the
document's own folder.

After a move, a tab stored by its full path keeps its full path instead of
being recomputed project-relative, so it keeps user-file access for save,
the next rename, image insert and restore after restart. Folder moves go
through the same remap.

Also un-export unused resolver exports and avoid a copy for single-chunk reads.
2026-10-04 16:55:32 -07:00
2026-09-26 20:50:46 +00:00
2026-05-04 20:42:03 -07:00
2026-03-16 22:27:51 -07:00
2026-03-28 10:19:14 -07:00

Orca Orca

GitHub stars Total downloads across all releases License: MIT Join the Orca Discord Follow Orca on X Supported platforms: macOS, Windows, and Linux

中文 · 日本語 · 한국어 · Español · Français · Português

The AI Orchestrator for 100x builders.
Run Codex, ClaudeCode, OpenCode or Pi side-by-side — each in its own worktree, tracked in one place.

Download Orca

Orca desktop app running agents in parallel worktrees, with the Orca mobile companion app in the corner

Features

Mobile Companion

Monitor and steer your agents from your phone — get notified when an agent finishes and send follow-ups from anywhere.

iOS App Store · Android APK 0.0.52 · Docs →

Orca desktop with the mobile companion app

Parallel Worktrees

Fan one prompt across five agents, each in its own isolated git worktree — compare the results and merge the winner.

Docs →

Parallel worktree orchestration

Terminal Splits

Ghostty-class terminals with WebGL rendering, infinite splits, and scrollback that survives restarts.

Docs →

Terminal splits

Design Mode

Click any UI element in a real Chromium window to send its HTML, CSS, and a cropped screenshot straight into your agent's prompt.

Docs →

Embedded browser and Design Mode

GitHub & Linear, Native

Browse PRs, issues, and project boards in-app — open a worktree from any task and review without a context switch.

Docs →

GitHub and Linear task workflows in Orca

SSH Worktrees

Run agents on a beefy remote box with full file editing, git, and terminals — auto-reconnect and port forwarding included.

Docs →

Remote worktrees over SSH

Annotate AI Diffs

Drop comments on any diff line and ship them back to the agent — review, edit, and commit without leaving Orca.

Docs →

Annotate AI-generated diffs

Drag Files to Agents

VS Code's editor with autosave everywhere — drag files or images straight into an agent prompt.

Docs →

Drag files and images into an agent prompt

Orca CLI

Agents drive Orca too — script every workflow with orca worktree create, snapshot, click, and fill.

Docs →

Script Orca from the CLI

Also in the box:

  • Quick open — Search across worktrees, files, agents, commands, and repo context without leaving your flow.
  • Account switcher & usage tracking — See Claude and Codex usage and rate-limit resets, and hot-swap accounts without re-logging in.
  • Rich repo previews — Preview Markdown, images, PDFs, and repo docs in the workspace.
  • Computer Use — Let agents operate desktop apps and visible UI when a workflow needs real interaction.
  • Notifications and unread state — Know when an agent finishes or needs attention, then mark threads unread to come back later.
  • And many, many more — we ship daily, so this list is perpetually behind. The changelog is the real feature list.

Supported Agents

Works with any CLI agent — if it runs in a terminal, it runs in Orca.

Claude Code logo Claude Code   Codex logo Codex   Grok logo Grok   Cursor logo Cursor   GitHub Copilot logo GitHub Copilot   Muse logo Muse   DeepSeek Harness logo DeepSeek Harness   ZCode logo ZCode   OpenCode logo OpenCode   MiMo Code logo MiMo Code   Amp logo Amp   OpenClaude logo OpenClaude   Antigravity logo Antigravity   Pi logo Pi   oh-my-pi logo oh-my-pi   Hermes Agent logo Hermes Agent   Devin logo Devin   Goose logo Goose   Auggie logo Auggie   Autohand Code logo Autohand Code   Charm logo Charm   Cline logo Cline   CodeBuddy logo CodeBuddy   Codebuff logo Codebuff   Freebuff logo Freebuff   Command Code logo Command Code   Continue logo Continue   Droid logo Droid   Kilocode logo Kilocode   Kimi logo Kimi   Kiro logo Kiro   Mistral Vibe logo Mistral Vibe   Qwen Code logo Qwen Code   Rovo Dev logo Rovo Dev   + any CLI agent


Install

Desktop — macOS, Windows, Linux

Or via a package manager:

# macOS (Homebrew)
brew install --cask stablyai/orca/orca

# Arch Linux (AUR) — or stably-orca-git to build from source
yay -S stably-orca-bin

Mobile Companion — iOS, Android

Pair with your desktop app to monitor and steer your agents from your phone.


Community & Support

  • Discord: Join the community on Discord.

  • Twitter / X: Follow @orca_build for updates and announcements.

  • WeChat: Scan to join the Orca community WeChat group 11.

    WeChat group 11 QR code for the Orca community
  • Feedback & Ideas: We ship fast. Missing something? Request a new feature.

  • Privacy: See the privacy & telemetry docs for what anonymous usage data Orca collects and how to opt out.

  • Show Support: Star this repo to follow along with our daily ships.


Developing

Want to contribute or run locally? See our CONTRIBUTING.md guide.

The relay that pairs the mobile app with a desktop host is also in this repository under cloud/, with a separate pnpm workspace and setup guide.

Orca contributors

GitHub star history chart for stablyai/orca

Signed Builds

Windows code signing sponored/provided by SignPath.io, certificate by SignPath Foundation.

License

Orca is free and open source under the MIT License.

S
Description
Orca is the ADE for working with a fleet of parallel agents. Run any coding agent with your own subscription. Available on desktop, mobile and remote runtime.
Readme MIT
1.7 GiB
Languages
TypeScript 95.2%
JavaScript 4.1%
Swift 0.2%
HCL 0.1%
CSS 0.1%