fix(cli): tool summaries show wrong paths when HOME=/ or the home is a path prefix (#162483)

Fixes #162438.

`shortenHomeInString` now replaces the home directory only at a path boundary. With HOME=/home/al, `/home/alice/...` is left alone; with HOME=/, slashes are no longer turned into `~`. `/home/al/x` still becomes `~/x` and the exact home path still becomes `~`.

Proof: real CLI in secretless Docker (`openclaw reset --scope config --dry-run`). main printed `~ice/p162438.json` and `~p162438~root.json`; this PR prints the correct paths. Regression test fails on main and passes here.

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
boeingchoco 2026-10-01 21:27:53 +09:00 • committed by GitHub
parent a9074f2454
commit e93bc05733
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 45 additions and 6 deletions

View file

@ -227,6 +227,29 @@ describe("shortenHomeInString", () => {
},
);
it.skipIf(process.platform === "win32")("replaces only whole home path components", () => {
withEnv({ OPENCLAW_HOME: undefined, HOME: "/home/al" }, () => {
expect(shortenHomeInString("/home/al")).toBe("~");
expect(shortenHomeInString("/home/al/x")).toBe("~/x");
expect(
shortenHomeInString("open /home/al/notes, /home/al, /home/alice/x and /mnt/home/al/x"),
).toBe("open ~/notes, ~, /home/alice/x and /mnt/home/al/x");
expect(shortenHomeInString("Stored in /home/al. Backup: /home/al.bak/x")).toBe(
"Stored in ~. Backup: /home/al.bak/x",
);
const siblingPaths =
"/home/al+old, /home/al./notes, /home/al,old/x, /home/al:old/x, /home/al\\ice/x, /mnt+/home/al/x";
expect(shortenHomeInString(siblingPaths)).toBe(siblingPaths);
expect(
shortenHomeInString('PATH=/home/al:/usr/bin:/home/al/bin; dirs=/tmp;/home/al ("/home/al")'),
).toBe('PATH=~:/usr/bin:~/bin; dirs=/tmp;~ ("~")');
expect(shortenHomeInString("at file:///home/al/app.js:5:3")).toBe("at file://~/app.js:5:3");
});
withEnv({ OPENCLAW_HOME: undefined, HOME: "/" }, () => {
expect(shortenHomeInString("see /var/log/openclaw.log")).toBe("see /var/log/openclaw.log");
});
});
it.skipIf(process.platform !== "win32")(
"shortens real Windows home casing aliases inside diagnostic text",
async () => {

View file

@ -1,5 +1,6 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { normalizeHomeDirValue } from "@openclaw/normalization-core/home-dir";
import { resolveConfigDir } from "./infra/config-dir.js";
import { resolveEffectiveHomeDir, resolveUserPath } from "./infra/home-dir.js";
@ -60,6 +61,13 @@ export function resolveHomeDir(): string | undefined {
return resolveEffectiveHomeDir(process.env, os.homedir);
}
// Stack traces print ESM paths as file:// URLs, so the URL scheme also starts a path.
const HOME_TEXT_START = String.raw`(?<=^|[\s"'\x60(\[{<=:;]|file://)`;
const HOME_TEXT_DELIMITER = String.raw`[\s"'\x60)\]}>]`;
// A PATH-style list continues with another absolute path, home prefix, or drive letter.
const HOME_TEXT_LIST_NEXT = String.raw`[:;](?=[/\\~$]|[A-Za-z]:)`;
function resolveHomeDisplayPrefix(): { home: string; prefix: string } | undefined {
const home = resolveHomeDir();
if (!home) {
@ -81,19 +89,27 @@ export function shortenHomePath(input: string): string {
return shortenPathWithHome(input, display);
}
/** Replaces all effective-home occurrences inside a diagnostic string. */
/** Replaces effective-home path occurrences inside a diagnostic string. */
export function shortenHomeInString(input: string): string {
if (!input) {
return input;
}
const display = resolveHomeDisplayPrefix();
if (!display) {
// A filesystem-root home such as "/" would turn every path separator into the prefix.
if (!display || path.parse(display.home).root === display.home) {
return input;
}
if (process.platform === "win32") {
return input.replace(new RegExp(escapeRegExpValue(display.home), "giu"), display.prefix);
}
return input.split(display.home).join(display.prefix);
// Diagnostics delimit paths with whitespace, quotes, brackets, `=`, and PATH list separators.
// Replace the home only between those delimiters so /home/al+old, /mnt/home/al, and
// /home/al.bak stay exact. Trailing `.`, `,`, `;`, or `:` ends the home only when a
// delimiter, the end of the text, or the next PATH entry follows it.
// POSIX file names may contain backslashes, so only Windows treats them as separators.
const pathSeparator = process.platform === "win32" ? String.raw`[\\/]` : "/";
const homePattern = new RegExp(
`${HOME_TEXT_START}${escapeRegExpValue(display.home)}(?=$|${pathSeparator}|${HOME_TEXT_DELIMITER}|[.,;:](?:$|${HOME_TEXT_DELIMITER})|${HOME_TEXT_LIST_NEXT})`,
process.platform === "win32" ? "giu" : "gu",
);
return input.replace(homePattern, display.prefix);
}
/** Shortens a path for display without changing non-home paths. */