Resolve host paths before mounting them, and stop reading stored data as choice
Four fixes that share a shape: a value already on disk, or one spelled
around a check, being taken at face value.
`/..` bind-mounted the entire host filesystem read-write. `is_filesystem_root`
was purely lexical — trim trailing separators, refuse what was left only if it
was empty or a bare `C:` — and nothing in the file ever called `canonicalize`,
so `/..`, `/./`, `/home/..`, `/etc/../` and `C:\..` all passed. The daemon
resolves them: `docker run -v /..:/mnt/probe` mounts the host root, and the app
mounts read-*write* into a container whose agent has passwordless sudo. It is
the escalation `check_mount_name_stays_under_workspace` exists to close,
reached through the host-path half of the mount instead of the mount-name half.
`classify_mount_source` replaces it and asks the OS: `canonicalize` applies
`..`, follows symlinks, and resolves 8.3 aliases and UNC spellings on Windows.
A path that cannot be resolved — `projects.json` synced from another machine,
a folder not created yet — falls back to a lexical collapse rather than being
refused, because refusing would make such a project unsavable; the gap is
bounded, since what resolution adds is a property of paths that exist. A path
that names no location at all (`C:x`, a relative path) is refused rather than
guessed at. Same check now guards `ssh_key_path` and `ca_cert_path`, whose
read-only mounts were whole-host disclosure at /tmp/.host-ssh.
Custom env var names had no charset check anywhere, so `BASH_FUNC_stat%%` —
bash's wire format for an exported shell function, body in the value — reached
the container environment verbatim. Latent today because the image's /bin/sh is
dash, but the pre-commit scrub runs `/bin/sh -c` as root and nothing pins that.
Keys are now shell identifiers, on the project and the global list both, with
the same grandfathering the folder rows get: a stored key is admitted, a new or
edited one is not.
The blank workspace row was persisted. The comment said it was dropped on save;
the code computed the filtered list and then saved the unfiltered one, so
"+ Add folder" plus a blur stored `{"Target": "/workspace/", "Source": ""}` and
the project could never be started or recreated again. Every save in the
section now goes through one filter, and a blur that changed nothing saves
nothing.
Widening the five `ClaudeCodeSettings` booleans to `Option<bool>` reinterpreted
every stored record. They were plain `bool`s that always serialised, so every
project ever saved carries an explicit `"env_scrub": false` that nobody chose —
and under the new merge that `Some(false)` beats a global `Some(true)`, where
the old rule let the global win. Upgrading silently turned five settings off,
"strip credentials from subprocess environments" among them. Deserialisation
now goes through a shim that dates the record by the presence of the
pre-widening `enable_session_recap` key and reads its `false`s as unset. The
fields skip serialising when unset, so an older binary can still parse
`projects.json` after a downgrade — a `null` would fail to parse and take the
whole list down, since `ProjectsStore` parses all-or-nothing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
+13
-8
@@ -162,23 +162,28 @@ export interface OpenAiCompatibleConfig {
|
||||
* project that is "inherit the global value", and on the global settings it is
|
||||
* "leave Claude Code's own default alone". `false` is a deliberate off, which
|
||||
* is what lets a project turn a globally-enabled setting back off.
|
||||
*
|
||||
* Every field is optional as well as nullable: the Rust struct skips
|
||||
* serialising a field it has no value for, so an object with nothing set at
|
||||
* this level arrives as `{}`. Absent and `null` mean the same thing, which is
|
||||
* why every read of one of these has to use `== null` rather than `=== null`.
|
||||
*/
|
||||
export interface ClaudeCodeSettings {
|
||||
/** `null` = let Claude Code choose the renderer; `"default"` = classic, `"fullscreen"` = alt-screen. */
|
||||
tui_mode: string | null;
|
||||
tui_mode?: string | null;
|
||||
/** `null` = unset, else `"low" | "medium" | "high" | "xhigh"`. Written as `effortLevel`. */
|
||||
effort: string | null;
|
||||
auto_scroll_disabled: boolean | null;
|
||||
effort?: string | null;
|
||||
auto_scroll_disabled?: boolean | null;
|
||||
/** Written as `viewMode: "focus"`. */
|
||||
focus_mode: boolean | null;
|
||||
show_thinking_summaries: boolean | null;
|
||||
focus_mode?: boolean | null;
|
||||
show_thinking_summaries?: boolean | null;
|
||||
/**
|
||||
* Turns the session recap **off**. Held in the disabled sense because Claude
|
||||
* Code's recap is on by default — see the Rust doc on `ClaudeCodeSettings`.
|
||||
*/
|
||||
session_recap_disabled: boolean | null;
|
||||
env_scrub: boolean | null;
|
||||
prompt_caching_1h: boolean | null;
|
||||
session_recap_disabled?: boolean | null;
|
||||
env_scrub?: boolean | null;
|
||||
prompt_caching_1h?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ContainerInfo {
|
||||
|
||||
Reference in New Issue
Block a user