From fe7c8b46a848c67d42d6ffb097eba6b1d32866ed Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 24 Oct 2026 08:28:33 -0500 Subject: [PATCH 1/6] fix(server): clone errors no longer show a token from the URL's query Git repeats the clone URL in its fatal errors, query included. The error tail kept for the user stripped userinfo only, so a token passed as ?access_token=... reached the clone error every client shows. Co-Authored-By: Claude Opus 5.5 --- .../SourceControlRepositoryService.test.ts | 33 +++++++++++++++++++ .../SourceControlRepositoryService.ts | 5 ++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 4a9105e9f5d9e..f4ea9ca7a9fbe 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -383,6 +383,39 @@ it.effect.each([ }), ); +it.effect("keeps a query token out of a failed clone's error text", () => + Effect.gen(function* () { + const error = yield* Effect.flip( + cloneInTemporaryDirectory( + { + execute: (input) => + Effect.gen(function* () { + // Git quotes the URL it was given, query included, and appends a slash. + yield* ( + input.progress?.onStderrLine?.( + "fatal: unable to access 'https://github.com/octocat/t3code.git?access_token=s3cret/': Could not resolve host: github.com", + ) ?? Effect.void + ); + return yield* new GitCommandError({ + operation: input.operation, + command: "git", + cwd: input.cwd, + detail: "Git command exited with a non-zero status.", + exitCode: 128, + }); + }), + }, + { remoteUrl: "https://github.com/octocat/t3code.git?access_token=s3cret" }, + ), + ); + + assert.strictEqual( + error.detail, + "fatal: unable to access 'https://github.com/octocat/t3code.git': Could not resolve host: github.com", + ); + }), +); + it.effect("strips embedded credentials from the remote URL it reports", () => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index ceb9ec33288de..f81a219221c33 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -138,9 +138,12 @@ function redactRemoteUrl(remoteUrl: string): string { // Userinfo may itself contain `@`; everything up to the last one before the // host boundary goes. const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/]+@/gi; +// A query is a token, as in redactRemoteUrl. Git quotes the URL and appends a +// `/`, so the query runs to the closing quote or whitespace. +const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s'"]*/gi; function redactUrlCredentials(text: string): string { - return text.replace(URL_WITH_USERINFO, "$1"); + return text.replace(URL_WITH_USERINFO, "$1").replace(URL_QUERY, "$1"); } /** From e9c6fe972c626dd5a4d7f4f1a66b1526fc381ef6 Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Sat, 24 Oct 2026 22:04:28 -0500 Subject: [PATCH 2/6] fix(server): a quote in a query token no longer leaves part of it in clone errors The query rule stopped at the first quote, so a token such as access_token=a'b left b in the failed-clone error text. A quoted query now runs to the last matching quote that closes git's quoted URL. Co-Authored-By: Claude Opus 5.5 --- .../SourceControlRepositoryService.test.ts | 10 +++++++--- .../sourceControl/SourceControlRepositoryService.ts | 9 +++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index f4ea9ca7a9fbe..f617b85484384 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -383,7 +383,11 @@ it.effect.each([ }), ); -it.effect("keeps a query token out of a failed clone's error text", () => +it.effect.each([ + ["a plain token", "s3cret"], + ["a token with an apostrophe", "s3'cret"], + ["a token with a double quote", 's3"cret'], +] as const)("keeps %s in the query out of a failed clone's error text", ([, token]) => Effect.gen(function* () { const error = yield* Effect.flip( cloneInTemporaryDirectory( @@ -393,7 +397,7 @@ it.effect("keeps a query token out of a failed clone's error text", () => // Git quotes the URL it was given, query included, and appends a slash. yield* ( input.progress?.onStderrLine?.( - "fatal: unable to access 'https://github.com/octocat/t3code.git?access_token=s3cret/': Could not resolve host: github.com", + `fatal: unable to access 'https://github.com/octocat/t3code.git?access_token=${token}/': Could not resolve host: github.com`, ) ?? Effect.void ); return yield* new GitCommandError({ @@ -405,7 +409,7 @@ it.effect("keeps a query token out of a failed clone's error text", () => }); }), }, - { remoteUrl: "https://github.com/octocat/t3code.git?access_token=s3cret" }, + { remoteUrl: `https://github.com/octocat/t3code.git?access_token=${token}` }, ), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index f81a219221c33..972c9ebe516a3 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -139,11 +139,16 @@ function redactRemoteUrl(remoteUrl: string): string { // host boundary goes. const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/]+@/gi; // A query is a token, as in redactRemoteUrl. Git quotes the URL and appends a -// `/`, so the query runs to the closing quote or whitespace. +// `/`. The token may itself hold a quote, so a quoted query runs to the last +// matching quote that closes the URL; an unquoted one runs to whitespace. +const QUOTED_URL_QUERY = /(['"])([a-z][a-z0-9+.-]*:\/\/[^\s?]*)\?.*\1(?=[\s:]|$)/gi; const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s'"]*/gi; function redactUrlCredentials(text: string): string { - return text.replace(URL_WITH_USERINFO, "$1").replace(URL_QUERY, "$1"); + return text + .replace(URL_WITH_USERINFO, "$1") + .replace(QUOTED_URL_QUERY, "$1$2$1") + .replace(URL_QUERY, "$1"); } /** From 6e9c8a6025c32854dc017f20c9063fc93f89f91f Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 26 Oct 2026 04:26:43 +0000 Subject: [PATCH 3/6] fix(server): redact clone query tokens before userinfo --- .../SourceControlRepositoryService.test.ts | 9 ++++++--- .../sourceControl/SourceControlRepositoryService.ts | 10 ++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index f617b85484384..faf34d4584f7a 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -387,8 +387,11 @@ it.effect.each([ ["a plain token", "s3cret"], ["a token with an apostrophe", "s3'cret"], ["a token with a double quote", 's3"cret'], + ["a host-only token with an at sign", "abc@secret"], ] as const)("keeps %s in the query out of a failed clone's error text", ([, token]) => Effect.gen(function* () { + const remote = + token === "abc@secret" ? "https://github.com" : "https://github.com/octocat/t3code.git"; const error = yield* Effect.flip( cloneInTemporaryDirectory( { @@ -397,7 +400,7 @@ it.effect.each([ // Git quotes the URL it was given, query included, and appends a slash. yield* ( input.progress?.onStderrLine?.( - `fatal: unable to access 'https://github.com/octocat/t3code.git?access_token=${token}/': Could not resolve host: github.com`, + `fatal: unable to access '${remote}?access_token=${token}/': Could not resolve host: github.com`, ) ?? Effect.void ); return yield* new GitCommandError({ @@ -409,13 +412,13 @@ it.effect.each([ }); }), }, - { remoteUrl: `https://github.com/octocat/t3code.git?access_token=${token}` }, + { remoteUrl: `${remote}?access_token=${token}` }, ), ); assert.strictEqual( error.detail, - "fatal: unable to access 'https://github.com/octocat/t3code.git': Could not resolve host: github.com", + `fatal: unable to access '${remote}': Could not resolve host: github.com`, ); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 57b245785f7a4..61ca3a8c2ea78 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -128,18 +128,16 @@ function redactRemoteUrl(remoteUrl: string): string { } // Userinfo can contain @; remove through the last one before the host boundary. -const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/]+@/gi; -// A query is a token, as in redactRemoteUrl. Git quotes the URL and appends a -// `/`. The token may itself hold a quote, so a quoted query runs to the last -// matching quote that closes the URL; an unquoted one runs to whitespace. +const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]+@/gi; +// Git leaves quotes inside query tokens unescaped; consume through the closing quote. const QUOTED_URL_QUERY = /(['"])([a-z][a-z0-9+.-]*:\/\/[^\s?]*)\?.*\1(?=[\s:]|$)/gi; const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s'"]*/gi; function redactUrlCredentials(text: string): string { return text - .replace(URL_WITH_USERINFO, "$1") .replace(QUOTED_URL_QUERY, "$1$2$1") - .replace(URL_QUERY, "$1"); + .replace(URL_QUERY, "$1") + .replace(URL_WITH_USERINFO, "$1"); } // The remote already exists after push failure; advise fixing access and pushing. From 3969ac03f112930300caea500df0f1819bece3b5 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 28 Oct 2026 04:44:20 +0000 Subject: [PATCH 4/6] fix(server): keep pathless clone query tokens out of errors --- .../SourceControlRepositoryService.test.ts | 11 ++++++++--- .../sourceControl/SourceControlRepositoryService.ts | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index faf34d4584f7a..995bdbae9e0ad 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -397,10 +397,15 @@ it.effect.each([ { execute: (input) => Effect.gen(function* () { - // Git quotes the URL it was given, query included, and appends a slash. + const cloneUrl = input.args.at(-2)!; + // Git treats @ in a pathless URL's query as userinfo, losing the query delimiter. + const diagnosticUrl = + cloneUrl === "https://github.com?access_token=abc@secret" + ? "https://secret" + : cloneUrl; yield* ( input.progress?.onStderrLine?.( - `fatal: unable to access '${remote}?access_token=${token}/': Could not resolve host: github.com`, + `fatal: unable to access '${diagnosticUrl}/': Could not resolve host: github.com`, ) ?? Effect.void ); return yield* new GitCommandError({ @@ -418,7 +423,7 @@ it.effect.each([ assert.strictEqual( error.detail, - `fatal: unable to access '${remote}': Could not resolve host: github.com`, + `fatal: unable to access '${remote}${token === "abc@secret" ? "/" : ""}': Could not resolve host: github.com`, ); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 61ca3a8c2ea78..11e5f58e53ec9 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -346,11 +346,13 @@ export const make = Effect.gen(function* () { if (stderrTail.length > 4) stderrTail.shift(); }); }; + // Git misreads @ in a pathless URL's query as userinfo and echoes its suffix as the host. + const normalizedCloneUrl = cloneUrl.replace(/^([a-z][a-z0-9+.-]*:\/\/[^/?#]+)\?/i, "$1/?"); yield* git .execute({ operation: "SourceControlRepositoryService.cloneRepository", cwd: path.dirname(destinationPath), - args: ["clone", "--progress", "--", cloneUrl, path.basename(destinationPath)], + args: ["clone", "--progress", "--", normalizedCloneUrl, path.basename(destinationPath)], timeoutMs: options?.timeoutMs === undefined ? CLONE_TIMEOUT_MS : options.timeoutMs, // Buffered output is unused and progress can be huge; cap it while retaining line callbacks. maxOutputBytes: 256 * 1024, From d71e4a79db518b3cadf7affce08d966c0d987630 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 28 Oct 2026 04:52:20 +0000 Subject: [PATCH 5/6] fix(server): redact quotes in unquoted clone query errors --- .../SourceControlRepositoryService.test.ts | 12 +++++++++--- .../sourceControl/SourceControlRepositoryService.ts | 4 ++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 995bdbae9e0ad..19d412ee96678 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -388,7 +388,9 @@ it.effect.each([ ["a token with an apostrophe", "s3'cret"], ["a token with a double quote", 's3"cret'], ["a host-only token with an at sign", "abc@secret"], -] as const)("keeps %s in the query out of a failed clone's error text", ([, token]) => + ["an unquoted token with an apostrophe", "s3'cret", true], + ["an unquoted token with a double quote", 's3"cret', true], +] as const)("keeps %s in the query out of a failed clone's error text", ([, token, unquoted]) => Effect.gen(function* () { const remote = token === "abc@secret" ? "https://github.com" : "https://github.com/octocat/t3code.git"; @@ -405,7 +407,9 @@ it.effect.each([ : cloneUrl; yield* ( input.progress?.onStderrLine?.( - `fatal: unable to access '${diagnosticUrl}/': Could not resolve host: github.com`, + unquoted + ? `fatal: ${diagnosticUrl}/info/refs not valid: is this a git repository?` + : `fatal: unable to access '${diagnosticUrl}/': Could not resolve host: github.com`, ) ?? Effect.void ); return yield* new GitCommandError({ @@ -423,7 +427,9 @@ it.effect.each([ assert.strictEqual( error.detail, - `fatal: unable to access '${remote}${token === "abc@secret" ? "/" : ""}': Could not resolve host: github.com`, + unquoted + ? `fatal: ${remote} not valid: is this a git repository?` + : `fatal: unable to access '${remote}${token === "abc@secret" ? "/" : ""}': Could not resolve host: github.com`, ); }), ); diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index 11e5f58e53ec9..f2ca65aceddf9 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -110,7 +110,7 @@ function toRepositoryInfo( }; } -// Redact credentials from client clone snapshots; git still receives the original URL. +// Redact credentials from client clone snapshots; git still receives the credentials. function redactRemoteUrl(remoteUrl: string): string { try { const url = new URL(remoteUrl); @@ -131,7 +131,7 @@ function redactRemoteUrl(remoteUrl: string): string { const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]+@/gi; // Git leaves quotes inside query tokens unescaped; consume through the closing quote. const QUOTED_URL_QUERY = /(['"])([a-z][a-z0-9+.-]*:\/\/[^\s?]*)\?.*\1(?=[\s:]|$)/gi; -const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s'"]*/gi; +const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s]*/gi; function redactUrlCredentials(text: string): string { return text From 0dc37295bea482c11d8642137e127db0e9ec2b87 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Wed, 28 Oct 2026 05:02:56 +0000 Subject: [PATCH 6/6] fix(server): redact queries after quoted clone paths --- .../sourceControl/SourceControlRepositoryService.test.ts | 7 ++++++- .../src/sourceControl/SourceControlRepositoryService.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts index 19d412ee96678..b0770b06d4830 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.test.ts @@ -390,10 +390,15 @@ it.effect.each([ ["a host-only token with an at sign", "abc@secret"], ["an unquoted token with an apostrophe", "s3'cret", true], ["an unquoted token with a double quote", 's3"cret', true], + ["an unquoted URL with an apostrophe in its path", "path-token", true], ] as const)("keeps %s in the query out of a failed clone's error text", ([, token, unquoted]) => Effect.gen(function* () { const remote = - token === "abc@secret" ? "https://github.com" : "https://github.com/octocat/t3code.git"; + token === "abc@secret" + ? "https://github.com" + : token === "path-token" + ? "https://github.com/octocat/t3code's.git" + : "https://github.com/octocat/t3code.git"; const error = yield* Effect.flip( cloneInTemporaryDirectory( { diff --git a/apps/server/src/sourceControl/SourceControlRepositoryService.ts b/apps/server/src/sourceControl/SourceControlRepositoryService.ts index f2ca65aceddf9..6bfa19e70385a 100644 --- a/apps/server/src/sourceControl/SourceControlRepositoryService.ts +++ b/apps/server/src/sourceControl/SourceControlRepositoryService.ts @@ -131,7 +131,7 @@ function redactRemoteUrl(remoteUrl: string): string { const URL_WITH_USERINFO = /\b([a-z][a-z0-9+.-]*:\/\/)[^\s/?#]+@/gi; // Git leaves quotes inside query tokens unescaped; consume through the closing quote. const QUOTED_URL_QUERY = /(['"])([a-z][a-z0-9+.-]*:\/\/[^\s?]*)\?.*\1(?=[\s:]|$)/gi; -const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?'"]*)\?[^\s]*/gi; +const URL_QUERY = /\b([a-z][a-z0-9+.-]*:\/\/[^\s?]*)\?[^\s]*/gi; function redactUrlCredentials(text: string): string { return text