Skip to content

Commit e746431

Browse files
yanyunl1991apurvvkumariaprekshivyas
authored
fix(backup): stop rejecting hard-linked package files before backup (#9317)
<!-- markdownlint-disable MD041 --> ## Summary A Hermes sandbox can contain multiply-linked regular files after installing lazy packages. The backup audit now records those files and archives each path as a separate regular file. Unsafe symlinks and special files still stop backup creation. ## Related Issue Closes #9314. ## Changes - Accept and report multiply-linked regular files during the pre-backup audit. - Add `tar --hard-dereference` so every included path becomes a regular-file archive entry that the safe restore path accepts. - Keep unsafe-symlink and special-file rejection unchanged. - Add a genuine linked-pair fixture, restored-content assertions, and regression coverage for the retained rejection behavior. - Correct the backup and restore documentation to describe the current security contract. ## Type of Change - [ ] Code change (feature, bug fix, or refactor) - [x] Code change with doc updates - [ ] Doc only (prose changes, no code sample modifications) - [ ] Doc only (includes code sample changes) ## Quality Gates - [x] Tests added or updated for changed behavior - [ ] Existing tests cover changed behavior — justification: - [ ] Tests not applicable — justification: - [x] Docs updated for user-facing behavior changes - [ ] Docs not applicable — justification: - [x] Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging) - [x] Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval: #9317 (comment) - [ ] Non-success, skipped, or missing CI check accepted by maintainer — not applicable; required CI and both Advisor lanes passed on the current revision: https://github.com/NVIDIA/NemoClaw/actions/runs/32054121014 ## Documentation Writer Review - [x] Documentation writer subagent reviewed the completed changes - Result: `docs-updated` - Evidence: `docs/manage-sandboxes/backup-restore.mdx`; `npm run docs` passed with 0 errors and 2 existing warnings; generated OpenClaw, Hermes, and Deep Agents variants contain the corrected wording - Agent: Codex Desktop <!-- docs-review-head-sha: 63e9dc0 --> <!-- docs-review-agents-blob-sha: b9fb6a9 --> ## DGX Station Hardware Evidence - [ ] Tested on DGX Station - Tested commit: - Station profile/scenario: - Result: - Supporting evidence: ## Verification - [x] PR description includes a `Signed-off-by:` line and every commit appears as `Verified` in GitHub - [x] Normal `pre-commit`, `commit-msg`, and `pre-push` hooks passed - [x] Targeted behavior tests pass for the current change set — the contributor reports 788 snapshot, state, and backup tests passing; GitHub Actions run 32039470523 passed `cli-tests`, `checks`, build/typecheck, installer integration, and all 12 CLI test shards - [ ] Applicable broad gate passed — not required for this focused backup change; required CI and targeted suites passed - [x] Quality Gates section completed with required justifications or waivers - [x] No secrets, API keys, or credentials committed - [x] `npm run docs` passed with 0 errors; 2 existing warnings are unchanged - [x] Doc pages follow the [style guide](https://github.com/NVIDIA/NemoClaw/blob/main/docs/CONTRIBUTING.md) - [ ] New doc pages include SPDX header and frontmatter (new pages only) ### Platform evidence The contributor reproduced the failure and verified the fix on a DGX Spark aarch64 host with a Hermes sandbox containing 224 multiply-linked lazy-package files. Three consecutive `backup-all` runs showed success before lazy-package installation, the reported failure afterward, and success with the change. The restored linked paths were regular files with independent link counts. --- Signed-off-by: Yanyun Liao <yanyunl@nvidia.com> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Backups now successfully include valid hard-linked files, archiving each path independently. * Hard-linked files are recorded and logged during backup audits. * Unsafe symlinks and special files continue to be rejected. * Audit messages now provide clearer reporting for unsupported file types. * **Documentation** * Updated backup and restore guidance to describe hard-linked file handling and safety checks. * **Tests** * Added coverage for hard-linked files, symlinks, special files, audit results, and backup behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Yanyun Liao <yanyunl@nvidia.com> Signed-off-by: Apurv Kumaria <akumaria@nvidia.com> Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com> Co-authored-by: Apurv Kumaria <akumaria@nvidia.com> Co-authored-by: Prekshi Vyas <prekshiv@nvidia.com>
1 parent 95d9e21 commit e746431

6 files changed

Lines changed: 250 additions & 15 deletions

File tree

docs/manage-sandboxes/backup-restore.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,8 @@ It clears the remaining local shields state only after deletion succeeds, before
231231
## Restore Agent Configuration Safely
232232

233233
The `$$nemoclaw <name> rebuild` command uses the same snapshot mechanism automatically.
234-
NemoClaw rejects unsafe symlinks and hard links inside sandbox state during backup creation before they can enter a snapshot.
234+
NemoClaw rejects unsafe symlinks and special files inside sandbox state during backup creation.
235+
It records multiply-linked regular files and archives each path as a separate regular file.
235236

236237
<AgentOnly variant="openclaw">
237238
Snapshot restore performs a targeted repair for legacy `.openclaw-data` symlinks that older images created.

src/lib/state/sandbox.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1483,10 +1483,21 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions =
14831483
if (existingDirs.length === 0) {
14841484
_log("No state dirs found in sandbox (all empty)");
14851485
} else {
1486-
// NC-2227-04: Pre-backup audit — reject symlinks, hardlinks, and special
1487-
// files inside state dirs. A compromised agent could plant a symlink like
1486+
// NC-2227-04: Pre-backup audit — reject symlinks and special files
1487+
// inside state dirs. A compromised agent could plant a symlink like
14881488
// workspace/copy -> ../openclaw.json to exfiltrate config via backup.
14891489
//
1490+
// Multiply-linked regular files are collected for observability but do
1491+
// not reject the backup (#9314). The archive command below uses
1492+
// `--hard-dereference`, so every included path is stored and restored
1493+
// as a plain regular file. It offers no exfiltration path the audit
1494+
// could close, because an agent that can create a hard link inside a
1495+
// state dir can equally `cp` the same bytes there, and a copy is an
1496+
// ordinary regular file this audit never sees. Rejecting hard links
1497+
// only broke legitimate installs: package managers hard-link from
1498+
// their cache, so every Hermes sandbox that lazily installed a
1499+
// dependency failed its pre-upgrade backup.
1500+
//
14901501
// The printf format emits "<type>\t<absPath>\t<linkTarget>" — %l is
14911502
// empty for non-symlinks but always present, so the field count is
14921503
// stable. Tab separator assumes state-dir paths don't contain tabs,
@@ -1531,6 +1542,7 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions =
15311542
if (auditOutput.length > 0) {
15321543
const allEntries = auditOutput.split("\n").filter((l) => l.length > 0);
15331544
const whitelisted: string[] = [];
1545+
const hardLinked: string[] = [];
15341546
const violations: string[] = [];
15351547
const dirPrefix = `${dir}/`;
15361548
for (const entry of allEntries) {
@@ -1545,6 +1557,11 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions =
15451557
: absPath;
15461558
if (type === "l" && isAllowedStateSymlink(relPath, linkTarget)) {
15471559
whitelisted.push(entry);
1560+
} else if (type === "f") {
1561+
// The audit's `find` only emits regular files through its
1562+
// `-links +1` branch, so a reported `f` row is a hard link.
1563+
// Recorded, not rejected — see the rationale above (#9314).
1564+
hardLinked.push(entry);
15481565
} else {
15491566
violations.push(entry);
15501567
}
@@ -1554,8 +1571,13 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions =
15541571
`Pre-backup audit whitelisted ${whitelisted.length} entries (image npm symlinks): ${whitelisted.slice(0, 5).join("; ")}`,
15551572
);
15561573
}
1574+
if (hardLinked.length > 0) {
1575+
_log(
1576+
`Pre-backup audit accepted ${hardLinked.length} multiply-linked regular files (archived as plain files): ${hardLinked.slice(0, 5).join("; ")}`,
1577+
);
1578+
}
15571579
if (violations.length > 0) {
1558-
// Non-whitelisted symlinks / hard links / special files — reject
1580+
// Non-whitelisted symlinks / special files — reject
15591581
_log(
15601582
`SECURITY: Pre-backup audit found ${violations.length} unsafe entries: ${violations.slice(0, 5).join("; ")}`,
15611583
);
@@ -1566,17 +1588,27 @@ export function backupSandboxState(sandboxName: string, options: BackupOptions =
15661588
failedDirs: [...existingDirs],
15671589
backedUpFiles,
15681590
failedFiles: stateFiles.map((f) => f.path),
1569-
error: `Pre-backup audit rejected: symlinks, hard links, or special files found in state dirs: ${violations.slice(0, 3).join("; ")}`,
1591+
error: `Pre-backup audit rejected: symlinks or special files found in state dirs: ${violations.slice(0, 3).join("; ")}`,
15701592
};
15711593
}
15721594
}
1573-
_log("Pre-backup audit passed — no unsafe symlinks, hard links, or special files found");
1595+
_log("Pre-backup audit passed — no unsafe symlinks or special files found");
15741596

15751597
// Download via SSH+tar
15761598
// NC-2227-04: Removed -h flag (was following symlinks). State dirs are
15771599
// now agent-writable and co-located with config — a compromised agent
15781600
// could create symlinks to exfiltrate config contents via backup.
1579-
const tarCmd = `tar -cf - -C ${shellQuote(dir)} -- ${existingDirs.map(shellQuote).join(" ")}`;
1601+
//
1602+
// `--hard-dereference` archives each multiply-linked path as its own
1603+
// regular file. Without it `tar` emits a hard-link record for the second
1604+
// and later paths sharing an inode, and `safeTarExtract` rejects those
1605+
// records — so a state dir holding two links to one inode would pass the
1606+
// audit and then fail while unpacking (#9314). It also keeps the archive
1607+
// self-describing: every entry restores as a plain file, matching what
1608+
// the audit now accepts. Note this is about links *within* the archived
1609+
// tree; a link whose other end lives outside it (a package manager
1610+
// linking out of its cache) already archives as a plain file.
1611+
const tarCmd = `tar --hard-dereference -cf - -C ${shellQuote(dir)} -- ${existingDirs.map(shellQuote).join(" ")}`;
15801612
_log(`Downloading via SSH+tar: ${tarCmd}`);
15811613
let downloadedTarDir: string | undefined;
15821614
let downloadedTarPath: string;

test/helpers/snapshot-state-discovery-fixture.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ if (cmd.includes("openclaw.json") && cmd.includes("cat --")) {
3636
if (cmd.includes("find ")) {
3737
process.exit(0);
3838
}
39-
if (cmd.includes("tar -cf -")) {
39+
if (cmd.includes("-cf -")) {
4040
const stagingDirs = fs.readdirSync(${JSON.stringify(stagingRoot)});
4141
const archivePaths = stagingDirs
4242
.map((entry) => require("node:path").join(${JSON.stringify(stagingRoot)}, entry, "archive.tar"))
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
/**
5+
* Pre-backup audit (NC-2227-04) treatment of multiply-linked regular files.
6+
*
7+
* Package managers hard-link installed files out of their cache, so a Hermes
8+
* sandbox that lazily installed a dependency carries hundreds of them under
9+
* `lazy-packages`. Rejecting those aborted the whole pre-upgrade backup
10+
* (#9314). They are now recorded and archived; symlink and special-file
11+
* rejection is unchanged.
12+
*/
13+
14+
import fs from "node:fs";
15+
import { spawnSync } from "node:child_process";
16+
import os from "node:os";
17+
import path from "node:path";
18+
import { pathToFileURL } from "node:url";
19+
import { afterAll, describe, expect, it } from "vitest";
20+
21+
const ORIGINAL_HOME = process.env.HOME;
22+
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-backup-audit-hardlinks-"));
23+
process.env.HOME = TMP_HOME;
24+
const tarHelp = spawnSync("tar", ["--help"], { encoding: "utf8" });
25+
const supportsHardDereference = `${tarHelp.stdout}${tarHelp.stderr}`.includes("--hard-dereference");
26+
// Production executes GNU tar inside the Linux sandbox. Skip only this
27+
// archive-behavior case on hosts such as macOS whose BSD tar lacks that flag.
28+
const hardDereferenceTest = supportsHardDereference ? it : it.skip;
29+
30+
const REPO_ROOT = path.join(import.meta.dirname, "..");
31+
type SandboxStateModule = typeof import("../src/lib/state/sandbox.js");
32+
const sandboxState = (await import(
33+
pathToFileURL(path.join(REPO_ROOT, "src", "lib", "state", "sandbox.ts")).href
34+
)) as SandboxStateModule;
35+
36+
function writeExecutable(filePath: string, source: string): void {
37+
fs.writeFileSync(filePath, source, { mode: 0o755 });
38+
}
39+
40+
/** Restore an env var without branching, mirroring the sibling snapshot tests. */
41+
function restoreEnv(name: string, value: string | undefined): void {
42+
value === undefined
43+
? Reflect.deleteProperty(process.env, name)
44+
: Reflect.set(process.env, name, value);
45+
}
46+
47+
function writeRegistry(sandboxName: string): void {
48+
fs.mkdirSync(path.join(TMP_HOME, ".nemoclaw"), { recursive: true });
49+
fs.writeFileSync(
50+
path.join(TMP_HOME, ".nemoclaw", "sandboxes.json"),
51+
JSON.stringify({
52+
defaultSandbox: sandboxName,
53+
sandboxes: {
54+
[sandboxName]: {
55+
name: sandboxName,
56+
model: "m",
57+
provider: "p",
58+
gpuEnabled: false,
59+
policies: [],
60+
agent: null,
61+
},
62+
},
63+
}),
64+
);
65+
}
66+
67+
function writeFakeOpenshell(binDir: string): string {
68+
const openshell = path.join(binDir, "openshell");
69+
writeExecutable(
70+
openshell,
71+
`#!/usr/bin/env node
72+
const args = process.argv.slice(2);
73+
if (args[0] === "sandbox" && args[1] === "ssh-config") {
74+
process.stdout.write("Host openshell-alpha\\n HostName 127.0.0.1\\n User sandbox\\n");
75+
process.exit(0);
76+
}
77+
process.exit(0);
78+
`,
79+
);
80+
return openshell;
81+
}
82+
83+
/**
84+
* Run `backupSandboxState` against a fake sandbox whose pre-backup audit
85+
* reports `auditLines` (raw `find -printf "%y\t%p\t%l\n"` rows).
86+
*/
87+
function backupWithAuditOutput(
88+
auditLines: string,
89+
): ReturnType<SandboxStateModule["backupSandboxState"]> {
90+
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-audit-fixture-"));
91+
const oldPath = process.env.PATH;
92+
const oldOpenshell = process.env.NEMOCLAW_OPENSHELL_BIN;
93+
try {
94+
const binDir = path.join(fixture, "bin");
95+
const stateRoot = path.join(fixture, "sandbox-root", ".openclaw");
96+
const existingDirs = ["workspace"];
97+
fs.mkdirSync(binDir, { recursive: true });
98+
for (const d of existingDirs) fs.mkdirSync(path.join(stateRoot, d), { recursive: true });
99+
fs.writeFileSync(path.join(stateRoot, "workspace", "note.txt"), "content\n");
100+
// A real multiply-linked pair inside the archived tree, the shape a package
101+
// manager leaves behind. `tar` would otherwise emit a hard-link record for
102+
// the second path and `safeTarExtract` would reject the archive.
103+
const linkedDir = path.join(stateRoot, "workspace", "lazy-packages");
104+
fs.mkdirSync(linkedDir, { recursive: true });
105+
fs.writeFileSync(path.join(linkedDir, "impl.py"), "package payload\n");
106+
fs.linkSync(path.join(linkedDir, "impl.py"), path.join(linkedDir, "alias.py"));
107+
108+
const openshell = writeFakeOpenshell(binDir);
109+
writeExecutable(
110+
path.join(binDir, "ssh"),
111+
`#!/usr/bin/env node
112+
const { spawnSync } = require("node:child_process");
113+
const fs = require("node:fs");
114+
const cmd = process.argv[process.argv.length - 1] || "";
115+
const existingDirs = ${JSON.stringify(existingDirs)};
116+
if (cmd.includes("[ -d ")) {
117+
process.stdout.write(existingDirs.join("\\n") + "\\n");
118+
process.exit(0);
119+
}
120+
if (cmd.includes("find ")) {
121+
process.stdout.write(${JSON.stringify(auditLines)} + (${JSON.stringify(auditLines)} ? "\\n" : ""));
122+
process.exit(0);
123+
}
124+
if (cmd.includes("tar ") && cmd.includes("-cf -")) {
125+
// Run the archive command the product actually issued, with the sandbox
126+
// state path mapped onto the fixture, so tar flags are exercised for real.
127+
const real = cmd.split("/sandbox/.openclaw").join(${JSON.stringify(stateRoot)});
128+
const r = spawnSync("sh", ["-c", real], { stdio: ["ignore", "pipe", "pipe"] });
129+
if (r.stdout) fs.writeSync(1, r.stdout);
130+
process.exit(r.status || 0);
131+
}
132+
process.exit(0);
133+
`,
134+
);
135+
136+
writeRegistry("alpha");
137+
process.env.NEMOCLAW_OPENSHELL_BIN = openshell;
138+
process.env.PATH = `${binDir}:${oldPath || ""}`;
139+
return sandboxState.backupSandboxState("alpha");
140+
} finally {
141+
restoreEnv("NEMOCLAW_OPENSHELL_BIN", oldOpenshell);
142+
restoreEnv("PATH", oldPath);
143+
fs.rmSync(fixture, { recursive: true, force: true });
144+
}
145+
}
146+
147+
afterAll(() => {
148+
restoreEnv("HOME", ORIGINAL_HOME);
149+
fs.rmSync(TMP_HOME, { recursive: true, force: true });
150+
});
151+
152+
describe("pre-backup audit — multiply-linked regular files (#9314)", () => {
153+
hardDereferenceTest(
154+
"backs up a sandbox whose state dirs contain hard-linked package files",
155+
() => {
156+
// Shape emitted by `find -type f -a -links +1`: type `f`, empty link
157+
// target. A lazily installed dependency hard-links out of the package
158+
// manager cache, so every installed file looks like this.
159+
const auditLines = [
160+
"f\t/sandbox/.openclaw/workspace/lazy-packages/aiohappyeyeballs/impl.py\t",
161+
"f\t/sandbox/.openclaw/workspace/lazy-packages/aiohappyeyeballs/utils.py\t",
162+
"f\t/sandbox/.openclaw/workspace/lazy-packages/edge_tts/__init__.py\t",
163+
].join("\n");
164+
165+
const backup = backupWithAuditOutput(auditLines);
166+
167+
expect(backup.success, backup.error).toBe(true);
168+
expect(backup.error).toBeUndefined();
169+
expect(backup.backedUpDirs).toEqual(["workspace"]);
170+
171+
// Both linked paths must survive as independent regular files: the archive
172+
// is unpacked through safeTarExtract, which rejects hard-link records.
173+
const linked = path.join(String(backup.manifest?.backupPath), "workspace", "lazy-packages");
174+
expect(fs.readFileSync(path.join(linked, "impl.py"), "utf8")).toBe("package payload\n");
175+
expect(fs.readFileSync(path.join(linked, "alias.py"), "utf8")).toBe("package payload\n");
176+
expect(fs.lstatSync(path.join(linked, "alias.py")).nlink).toBe(1);
177+
},
178+
);
179+
180+
it("still rejects an unsafe symlink alongside hard-linked files", () => {
181+
// Regression lock: accepting hard links must not weaken symlink rejection.
182+
const auditLines = [
183+
"f\t/sandbox/.openclaw/workspace/lazy-packages/edge_tts/__init__.py\t",
184+
"l\t/sandbox/.openclaw/workspace/escape\t../openclaw.json",
185+
].join("\n");
186+
187+
const backup = backupWithAuditOutput(auditLines);
188+
189+
expect(backup.success).toBe(false);
190+
expect(backup.error).toMatch(/Pre-backup audit rejected/);
191+
expect(backup.error).toContain("workspace/escape");
192+
});
193+
194+
it("still rejects special files", () => {
195+
// Regression lock: sockets/fifos/devices remain violations.
196+
const backup = backupWithAuditOutput("s\t/sandbox/.openclaw/workspace/agent.sock\t");
197+
198+
expect(backup.success).toBe(false);
199+
expect(backup.error).toMatch(/Pre-backup audit rejected/);
200+
expect(backup.error).toContain("agent.sock");
201+
});
202+
});

test/snapshot-runtime-auth-state.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ if (cmd.startsWith("{ ") && cmd.includes("printf")) {
9191
// Backup: pre-backup symlink/hardlink audit — fixture has none.
9292
if (cmd.includes("-printf")) { process.exit(0); }
9393
// Backup: tar download of state dirs.
94-
if (cmd.startsWith("tar -cf - -C ")) {
94+
if (cmd.startsWith("tar ") && cmd.includes("-cf - -C ")) {
9595
const names = [...cmd.matchAll(/'([^']+)'/g)].map((m) => m[1]);
9696
const result = spawnSync("tar", ["-cf", "-", "-C", mapPath(names[0]), "--", ...names.slice(1)], {
9797
stdio: ["ignore", "pipe", "pipe"],

test/snapshot.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -778,7 +778,7 @@ if (cmd.includes("openclaw.json") && cmd.includes("cat --")) {
778778
if (cmd.includes("find ")) {
779779
process.exit(0);
780780
}
781-
if (cmd.includes("tar -cf -")) {
781+
if (cmd.includes("-cf -")) {
782782
const r = spawnSync("tar", ["-cf", "-", "-C", ${JSON.stringify(openclawDir)}, ...existingDirs], {
783783
stdio: ["ignore", "pipe", "pipe"],
784784
});
@@ -875,7 +875,7 @@ if (cmd.includes("find ")) {
875875
process.stdout.write(${JSON.stringify(auditLines)} + "\\n");
876876
process.exit(0);
877877
}
878-
if (cmd.includes("tar -cf -")) {
878+
if (cmd.includes("-cf -")) {
879879
const r = spawnSync("tar", ["-cf", "-", "-C", ${JSON.stringify(openclawDir)}, ...existingDirs], {
880880
stdio: ["ignore", "pipe", "pipe"],
881881
});
@@ -939,7 +939,7 @@ if (cmd.includes("find ")) {
939939
process.stdout.write(${JSON.stringify(auditLines)} + "\\n");
940940
process.exit(0);
941941
}
942-
if (cmd.includes("tar -cf -")) {
942+
if (cmd.includes("-cf -")) {
943943
const r = spawnSync("tar", ["-cf", "-", "-C", openclawDir, ...existingDirs], {
944944
stdio: ["ignore", "pipe", "pipe"],
945945
});
@@ -1158,7 +1158,7 @@ if (cmd.includes("[ -d ")) {
11581158
if (cmd.includes("find ")) {
11591159
process.exit(0);
11601160
}
1161-
if (cmd.includes("tar -cf -")) {
1161+
if (cmd.includes("-cf -")) {
11621162
const r = spawnSync("tar", ["-cf", "-", "-C", ${JSON.stringify(openclawDir)}, "extensions"], {
11631163
stdio: ["ignore", "pipe", "pipe"],
11641164
});
@@ -1238,7 +1238,7 @@ if (cmd.includes("find ")) {
12381238
}
12391239
process.exit(0);
12401240
}
1241-
if (cmd.includes("tar -cf -")) {
1241+
if (cmd.includes("-cf -")) {
12421242
const r = spawnSync("tar", ["-cf", "-", "-C", ${JSON.stringify(openclawDir)}, ...existingDirs], {
12431243
stdio: ["ignore", "pipe", "pipe"],
12441244
});
@@ -1407,7 +1407,7 @@ if (cmd.includes("[ -d ")) {
14071407
if (cmd.includes("find ")) {
14081408
process.exit(0);
14091409
}
1410-
if (cmd.includes("tar -cf -")) {
1410+
if (cmd.includes("-cf -")) {
14111411
const r = spawnSync("tar", ["-cf", "-", "-C", deepAgentsDir, ".state", "skills", "agent/skills"], {
14121412
stdio: ["ignore", "pipe", "pipe"],
14131413
});

0 commit comments

Comments
 (0)