From c3fc029b1d9692abfb31b4391488084a2fe774bd Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 1 Jul 2026 06:18:08 -0700 Subject: [PATCH 1/2] Nest workspace under `workspace/` in project backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backup archive placed the workspace at the archive root (`./...`) while the sanitized home config sat under `home-claude/`. On extraction the workspace files scattered loose into the extraction directory and only `home-claude/` showed up as a distinct folder, so the backup read as "config only, workspace missing" — and some archive viewers didn't surface the root-level entries at all. Add `--transform='flags=r;s,^\./,workspace/,'` so the workspace nests under `workspace/`, parallel to `home-claude/`. `flags=r` scopes the rewrite to member names only, leaving symlink targets (relative and absolute) intact. Excludes still match the pre-transform names. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src-tauri/src/commands/file_commands.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src-tauri/src/commands/file_commands.rs b/app/src-tauri/src/commands/file_commands.rs index 9e48f20..83bc5e7 100644 --- a/app/src-tauri/src/commands/file_commands.rs +++ b/app/src-tauri/src/commands/file_commands.rs @@ -155,7 +155,7 @@ pub async fn download_container_file( /// Create a `.tar.gz` backup of the container and stream it to a host file. /// The archive contains: /// - the workspace (default /workspace), minus regenerable build artifacts -/// (node_modules, target), at the archive root, and +/// (node_modules, target), under `workspace/`, and /// - a sanitized copy of the home config under `home-claude/`: ~/.claude.json /// with secret-bearing keys removed (mcpServers/settings kept) and ~/.claude/ /// minus the OAuth `.credentials.json`, so MCP servers, settings and skills @@ -204,6 +204,10 @@ pub async fn download_container_backup( // transient unreadable file from aborting the whole backup. If jq can't // parse ~/.claude.json we substitute an empty object — never the raw file — // so secrets can't leak through the sanitization fallback. + // The `--transform` nests the workspace under `workspace/` (parallel to + // `home-claude/`) so an extracted archive has both clearly labeled instead + // of scattering the workspace files into the extraction dir. `flags=r` + // scopes the rewrite to member names only, leaving symlink targets intact. let script = r#"set -e STAGE=$(mktemp -d) trap 'rm -rf "$STAGE"' EXIT @@ -221,6 +225,7 @@ if [ -d "$HOME/.claude" ]; then fi tar czf - --ignore-failed-read \ --exclude='*/node_modules' --exclude='*/target' \ + --transform='flags=r;s,^\./,workspace/,' \ -C "$TC_BACKUP_SRC" . \ -C "$STAGE" home-claude"#; From 5cd528a4eff67f4da6e605f403b13d617f358b78 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 1 Jul 2026 06:22:14 -0700 Subject: [PATCH 2/2] Use flags=rh so intra-workspace hardlinks survive the transform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review caught that `flags=r` disables rewriting of both symlink AND hardlink target names. Leaving symlink targets alone is intended, but a hardlink's stored target is an archive-internal reference to another member's name — when member names become `workspace/...` but the hardlink target stays `./hard_link`, extraction fails hard: tar: workspace/file.txt: Cannot hard link to './hard_link': No such file or directory `flags=rh` rewrites regular member names and hardlink target names together (keeping the pair consistent) while still leaving symlink targets untouched. Verified in-container: extract exit 0, symlink target preserved, hardlink pair shares one inode, nesting under workspace/ intact. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/src-tauri/src/commands/file_commands.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src-tauri/src/commands/file_commands.rs b/app/src-tauri/src/commands/file_commands.rs index 83bc5e7..200b082 100644 --- a/app/src-tauri/src/commands/file_commands.rs +++ b/app/src-tauri/src/commands/file_commands.rs @@ -206,8 +206,10 @@ pub async fn download_container_backup( // so secrets can't leak through the sanitization fallback. // The `--transform` nests the workspace under `workspace/` (parallel to // `home-claude/`) so an extracted archive has both clearly labeled instead - // of scattering the workspace files into the extraction dir. `flags=r` - // scopes the rewrite to member names only, leaving symlink targets intact. + // of scattering the workspace files into the extraction dir. `flags=rh` + // rewrites regular member names AND hardlink target names (so an intra- + // workspace hardlink pair still resolves on extract) while leaving symlink + // targets untouched (rewriting those would corrupt relative/absolute links). let script = r#"set -e STAGE=$(mktemp -d) trap 'rm -rf "$STAGE"' EXIT @@ -225,7 +227,7 @@ if [ -d "$HOME/.claude" ]; then fi tar czf - --ignore-failed-read \ --exclude='*/node_modules' --exclude='*/target' \ - --transform='flags=r;s,^\./,workspace/,' \ + --transform='flags=rh;s,^\./,workspace/,' \ -C "$TC_BACKUP_SRC" . \ -C "$STAGE" home-claude"#;