Include sanitized home config in project backup
Extends download_container_backup to also capture the container's home config so MCP servers, settings, and skills set up directly via Claude Code (stored in ~/.claude.json / ~/.claude, on the home/config volumes that a Reset wipes) survive a backup/restore cycle. Secrets are stripped per the "exclude secrets" choice: ~/.claude.json is filtered through jq to drop primaryApiKey/oauthAccount/customApiKeyResponses (mcpServers and settings are kept), and ~/.claude/.credentials.json (the OAuth tokens) is omitted. Staged config is archived under home-claude/ in the tarball. Verified on the Ubuntu/jq container base. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -152,10 +152,17 @@ pub async fn download_container_file(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a `.tar.gz` backup of the container's /workspace and stream it to a
|
/// Create a `.tar.gz` backup of the container and stream it to a host file.
|
||||||
/// host file. Regenerable build artifacts (node_modules, target, .git/objects)
|
/// The archive contains:
|
||||||
/// are excluded so the archive stays restore-sized. Requires the container to
|
/// - the workspace (default /workspace), minus regenerable build artifacts
|
||||||
/// exist (it can be stopped or running). Returns the number of bytes written.
|
/// (node_modules, target, .git/objects), at the archive root, 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
|
||||||
|
/// set up via Claude Code survive a Reset.
|
||||||
|
/// Build + gzip happen inside the container so a large workspace isn't streamed
|
||||||
|
/// in full. Requires the container to exist (running or stopped). Returns the
|
||||||
|
/// number of bytes written.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn download_container_backup(
|
pub async fn download_container_backup(
|
||||||
project_id: String,
|
project_id: String,
|
||||||
@@ -176,22 +183,29 @@ pub async fn download_container_backup(
|
|||||||
let docker = get_docker()?;
|
let docker = get_docker()?;
|
||||||
let path = container_path.unwrap_or_else(|| "/workspace".to_string());
|
let path = container_path.unwrap_or_else(|| "/workspace".to_string());
|
||||||
|
|
||||||
// Build + gzip the archive inside the container (excludes happen there, so a
|
// Stage a sanitized home config, then tar+gzip workspace + staged config to
|
||||||
// 16 GB workspace doesn't get streamed in full), then pipe stdout to the
|
// stdout. mktemp/jq output go nowhere near stdout, so the only thing the
|
||||||
// chosen host file. --ignore-failed-read keeps a transient unreadable file
|
// exec emits on stdout is the archive itself. --ignore-failed-read keeps a
|
||||||
// from aborting the whole backup.
|
// transient unreadable file from aborting the whole backup.
|
||||||
let cmd = vec![
|
let script = r#"set -e
|
||||||
"tar".to_string(),
|
STAGE=$(mktemp -d)
|
||||||
"czf".to_string(),
|
mkdir -p "$STAGE/home-claude"
|
||||||
"-".to_string(),
|
if [ -f "$HOME/.claude.json" ]; then
|
||||||
"--ignore-failed-read".to_string(),
|
jq 'del(.primaryApiKey, .oauthAccount, .customApiKeyResponses)' "$HOME/.claude.json" \
|
||||||
"--exclude=*/node_modules".to_string(),
|
> "$STAGE/home-claude/.claude.json" 2>/dev/null \
|
||||||
"--exclude=*/target".to_string(),
|
|| cp "$HOME/.claude.json" "$STAGE/home-claude/.claude.json"
|
||||||
"--exclude=*/.git/objects".to_string(),
|
fi
|
||||||
"-C".to_string(),
|
if [ -d "$HOME/.claude" ]; then
|
||||||
path,
|
cp -a "$HOME/.claude" "$STAGE/home-claude/.claude" 2>/dev/null || true
|
||||||
".".to_string(),
|
rm -f "$STAGE/home-claude/.claude/.credentials.json"
|
||||||
];
|
fi
|
||||||
|
tar czf - --ignore-failed-read \
|
||||||
|
--exclude='*/node_modules' --exclude='*/target' --exclude='*/.git/objects' \
|
||||||
|
-C "$TC_BACKUP_SRC" . \
|
||||||
|
-C "$STAGE" home-claude
|
||||||
|
rm -rf "$STAGE""#;
|
||||||
|
|
||||||
|
let cmd = vec!["sh".to_string(), "-c".to_string(), script.to_string()];
|
||||||
|
|
||||||
let exec = docker
|
let exec = docker
|
||||||
.create_exec(
|
.create_exec(
|
||||||
@@ -200,6 +214,10 @@ pub async fn download_container_backup(
|
|||||||
attach_stdout: Some(true),
|
attach_stdout: Some(true),
|
||||||
attach_stderr: Some(true),
|
attach_stderr: Some(true),
|
||||||
cmd: Some(cmd),
|
cmd: Some(cmd),
|
||||||
|
env: Some(vec![
|
||||||
|
"HOME=/home/claude".to_string(),
|
||||||
|
format!("TC_BACKUP_SRC={}", path),
|
||||||
|
]),
|
||||||
user: Some("claude".to_string()),
|
user: Some("claude".to_string()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user