Ship the Files tab container-side only

Four successive audits found the same thing: host filesystem paths crossing
IPC is where the criticals in this work live. The most recent one found the
`link(2)` upload reservation returning success against a *directory* (linking
into it, leaving permanent stray files, and via a symlink-to-directory writing
outside the validated write root), failing every upload permanently on any
filesystem without hard links, and the post-resolution credential check
weakened from a general rule to an eleven-name denylist.

Rather than fix that a fifth time, the Files tab ships as what it is good at:
a browser, viewer and renamer that never touches the host.

Removed: `upload_file_to_container`, `download_container_file`, and everything
that existed only for them — the whole reservation (`UPLOAD_RESERVATION_SCRIPT`,
`reserve_upload_destination`, the placeholder rollback, `exec_oneshot_as_within`
which had no other caller), `stream_container_file_to_host`, `ChannelReader`,
`save_to_host`, the download ceiling, and the collision marker with its
frontend contract. On the frontend: the upload button, the pane's
`onDragDropEvent` handler, both "Save to host…" affordances, `uploadPaths` /
`downloadFile` / the overwrite prompt, and `OverwriteConfirmModal`.
`lib/uploadErrors.ts` is now `lib/refusalText.ts` and keeps only the half that
turns any backend refusal into the sentence a person reads.

Kept, and not weakened: `upload_host_file_to_terminal` and
`download_container_backup`. They predate this work, their hardening is a real
improvement over main, and they are now the whole answer to "how do I get a
file in or out" — drop it on the Terminal, or Back up container. The drop gate
(`lib/dropTarget.ts`, `PaneVisibility`) is untouched.

`resolve_host_path` gets the general hidden-component rule back. Round 3
replaced it with `HOST_CREDENTIAL_DIRS`, which is allow-by-omission for the
rest of `$HOME`: `~/.local/bin` (write there and you own the user's next shell
command), `~/.password-store`, browser profiles and `~/.pki/nssdb` were all
reachable through a planted symlink with a visible name — verified against a
real home directory, and all five refused now. It over-catches `.pnpm` and
`~/.cache`; for two occasional callers that is the cheaper mistake, and the
refusal says which folder it resolved through.

Two defects fixed while in here:

  * A symlinked directory listed as empty. `find` defaults to `-P`, which does
    not follow a symlink even as the starting point, so `-mindepth 1` discarded
    the only match and a real directory rendered as "Empty directory" — a
    first-order defect now that browsing *is* the feature. `-H` follows the
    starting point and nothing else, so a loop is `ELOOP` rather than a walk
    that does not end; verified against a live container for a symlinked
    directory, a broken link and a loop. `find`'s errno for the loop case is
    now a sentence.
  * `finish_download`'s replace path fired on *any* rename failure with a
    destination present — a vanished partial, a permission error, a directory
    at the destination — and deleted the user's file to complete a move that
    could not complete. It is now fenced to Windows (where a rename onto an
    existing path genuinely fails) and to a partial that still exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
2026-08-23 17:05:56 -07:00
co-authored by Claude Opus 5
parent 168b61d632
commit 06ccb4d818
19 changed files with 761 additions and 2928 deletions
+36 -9
View File
@@ -228,7 +228,7 @@ buttons. Below that are six tabs:
| **Sessions** | Past Claude Code conversations stored on this project's config volume, each with a **Resume** button |
| **Automation** | The scheduled tasks running inside this container — see [Automation & Scheduled Tasks](#automation--scheduled-tasks) |
| **Config** | All per-project configuration — see [Project Configuration](#project-configuration) |
| **Files** | Browse, download and upload files inside the container |
| **Files** | Browse, view and rename files inside the container — see [Files](#files) for how files get in and out |
| **Browser** | Watch — and take over — the browser Claude is driving with Playwright, see [The Browser Tab](#the-browser-tab) |
### Sessions
@@ -351,7 +351,7 @@ it. The sidebar row carries only the two hover controls.
| **Force stop** | Project Home header | Starting / Stopping | Interrupts a transition that is stuck |
| **Open Claude Terminal** | Project Home header; sidebar hover control; `Ctrl+T` | Running | Opens a new Claude Code terminal tab |
| **Shell** | Project Home header | Running | Opens a bash login shell tab in the container (no Claude Code) |
| **Files** | Project Home header, and the **Files** tab | Running | Switches to the Files tab to browse, download and upload files |
| **Files** | Project Home header, and the **Files** tab | Running | Switches to the Files tab to browse, view and rename files inside the container |
| **Config** | The **Config** tab | Always | Per-project configuration (most fields need the container stopped) |
| **Back up container** | **⋯** overflow menu | A container exists | Saves a `.tar.gz` archive of the container to a location you choose |
| **Reset container…** | **⋯** overflow menu | Stopped or Error | Destroys the container, snapshot image and both volumes, then recreates from the base image (wipes `~/.claude`) — asks first |
@@ -1161,16 +1161,43 @@ When you scroll up in the terminal to review previous output, a **Jump to Curren
### Files
The **Files** tab of Project Home browses inside a running container. You can:
The **Files** tab of Project Home browses inside a running container. It works entirely on the
container side — it never reads or writes anything on your own machine. You can:
- **Browse** the container filesystem, starting at `/workspace`, with breadcrumb navigation
- **Save to host…** — copy any file out to a location you pick. This is the way to get a file out
of a container; there is one button per file entry, and the file viewer offers it too
- **Upload file** from your host into the current container directory — or **drop files straight
onto the pane** from your desktop, which uploads them into the directory on screen
- **Browse** the container filesystem, starting at `/workspace`, with breadcrumb navigation.
Double-click a folder to open it, or the `..` row to go up; the arrow keys, Home and End move
between rows and Enter opens the selected one
- **View** a file — double-click it, or press Enter. Text files and images render in a read-only
viewer
- **Rename** an entry, from the row's Rename button or by pressing `F2`. A rename never moves a
file between folders
- **New folder** in the directory on screen
- **Refresh** the directory listing at any time
The listing shows file names, sizes, and modification dates.
The listing shows file names, sizes, and modification dates, and marks symbolic links.
#### Getting files in and out
The Files tab deliberately does **not** copy files between your computer and the container. There
are two supported routes, and they are the ones to use:
- **To get a file in:** drag it from your desktop and **drop it onto the Terminal tab**. The file
is copied into the container and its path is typed into the terminal for you, ready to hand to
Claude Code. (You can also drop it onto the terminal's *Following* toggle — the whole pane is a
drop target.)
- **To get files out:** use **Back up container** in Project Home's **⋯** overflow menu. It writes
a `.tar.gz` of the workspace and the container's `~/.claude` config to a location you choose.
For a single file, `cat` it in a terminal, or work in a project folder that is mounted from your
host in the first place — those files are already on both sides.
Both routes refuse a destination whose path passes through a hidden folder — anything with a
component beginning with `.`, such as `~/.ssh`, `~/.local/bin` or `~/.config`. That rule catches
more than it strictly needs to (a path that happens to resolve through `~/.cache` or
`node_modules/.pnpm` is refused as well), and the refusal says which folder tripped it. Choose a
visible location such as `~/Documents` or `~/Downloads`.
If you already keep the project in a folder mounted into the container, the simplest answer is
usually neither of the above: edit the file on your host and it is already inside.
### Terminal Rendering