From 7a9621f1e5099b69bc29fe50e8537b3c665a7a9e Mon Sep 17 00:00:00 2001 From: Harsh Bhanushali Date: Wed, 14 Oct 2026 07:44:57 +0000 Subject: [PATCH] Expand ~ in path options from the config file The shell expands ~ in command-line arguments, but values read from config.yaml were passed to path.resolve as-is, so `user-data-dir: ~/.config/code-server-data` resolved to `/~/.config/code-server-data`. Fixes #7051 --- src/node/cli.ts | 4 ++-- src/node/util.ts | 15 +++++++++++++++ test/unit/node/cli.test.ts | 12 ++++++++++++ test/unit/node/util.test.ts | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/node/cli.ts b/src/node/cli.ts index b7b71f471ddb..6597a3b6ae6b 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -3,7 +3,7 @@ import * as crypto from "crypto" import { promises as fs } from "fs" import { load } from "js-yaml" import * as path from "path" -import { generateCertificate, generatePassword, paths, splitOnFirstEquals } from "./util" +import { expandTilde, generateCertificate, generatePassword, paths, splitOnFirstEquals } from "./util" import { EditorSessionManagerClient } from "./vscodeSocket" export enum Feature { @@ -466,7 +466,7 @@ export const parse = ( } if (option.path) { - value = path.resolve(value) + value = path.resolve(expandTilde(value)) } switch (option.type) { diff --git a/src/node/util.ts b/src/node/util.ts index b7c0c3fa5339..8befd9941426 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -511,3 +511,18 @@ export function splitOnFirstEquals(str: string): [string, string | undefined] { const split = str.split(/=(.+)?/, 2) return [split[0], split[1]] } + +/** + * Expand a leading `~` to the home directory. The shell does this for + * command-line arguments but values from the config file are not expanded. + * `~user` forms are left alone. + */ +export function expandTilde(p: string): string { + if (p === "~") { + return os.homedir() + } + if (p.startsWith("~/") || p.startsWith(`~${path.sep}`)) { + return path.join(os.homedir(), p.slice(2)) + } + return p +} diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 628dcd98fabf..d31a35fbe2d9 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -1,5 +1,6 @@ import { Level, logger } from "@coder/logger" import { promises as fs } from "fs" +import * as os from "os" import * as path from "path" import { UserProvidedArgs, @@ -575,6 +576,17 @@ describe("parser", () => { "install-extension": ["extension.number1", "extension.number2"], }) }) + it("should expand ~ in path options from the config file", () => { + const configContents = ` + user-data-dir: ~/.config/code-server-data + extensions-dir: ~/extensions + ` + expect(parseConfigFile(configContents, "/fake-config-path")).toEqual({ + config: "/fake-config-path", + "user-data-dir": path.join(os.homedir(), ".config/code-server-data"), + "extensions-dir": path.join(os.homedir(), "extensions"), + }) + }) it("should ignore optional strings set to false", async () => { expect(parse(["--cert=false"])).toEqual({}) }) diff --git a/test/unit/node/util.test.ts b/test/unit/node/util.test.ts index 572c512a0f04..d92f9e1339d5 100644 --- a/test/unit/node/util.test.ts +++ b/test/unit/node/util.test.ts @@ -1,5 +1,6 @@ import * as cp from "child_process" import { promises as fs } from "fs" +import * as os from "os" import * as path from "path" import { generateUuid } from "../../../src/common/util" import { tmpdir } from "../../../src/node/constants" @@ -623,3 +624,18 @@ describe("splitOnFirstEquals", () => { }) }) }) + +describe("expandTilde", () => { + it("should expand a bare ~", () => { + expect(util.expandTilde("~")).toBe(os.homedir()) + }) + it("should expand a leading ~/", () => { + expect(util.expandTilde("~/.config/code-server")).toBe(path.join(os.homedir(), ".config/code-server")) + }) + it("should leave other paths alone", () => { + expect(util.expandTilde("/tmp/~/data")).toBe("/tmp/~/data") + expect(util.expandTilde("relative/~")).toBe("relative/~") + expect(util.expandTilde("~user/data")).toBe("~user/data") + expect(util.expandTilde("")).toBe("") + }) +})