From b64f7a3e9750e1ca50aaa46af89bb0a33ab61a19 Mon Sep 17 00:00:00 2001 From: Ignat Remizov Date: Thu, 26 Mar 2026 22:55:43 +0200 Subject: [PATCH 1/2] fix(hooks): materialize proxied hook stdin into a temp file Avoid forwarding hook stdin through `/dev/stdin` or `/proc/self/fd/0` when GitHub Desktop Plus runs Git hooks through the process proxy on Linux. - create a temporary FIFO under the system temp directory for Linux hook stdin forwarding instead of relying on fd-backed pseudo-paths that `git hook run` may fail to reopen - start the FIFO writer only after `spawn(...)` succeeds so pre-spawn failures cannot deadlock on a writer waiting for its first reader - attach an immediate handler to the FIFO forward promise so early reader disconnects or aborts cannot surface as unhandled promise rejections - surface unexpected stdin transport failures before reporting hook success, while still treating broken-pipe and abort conditions as expected during teardown - stream the proxy connection stdin into that FIFO so hook stdin behavior stays streaming instead of fully buffering the payload before hook startup - keep non-Linux behavior on `/dev/stdin` unchanged - register the proxy abort handler before stdin forwarding work begins so GUI-side cancellation still propagates during hook setup - wrap the temporary FIFO lifecycle in `try/finally`, abort the writer on early exit, and always clean up the temp directory after hook execution - treat hook completion as authoritative instead of waiting for the FIFO writer to drain, so hooks that stop reading stdin early do not hang the proxy on successful exit Behavioral effect: Hooks that depend on stdin still receive the expected payload, but Linux GUI clients no longer depend on reopening `/dev/stdin` or `/proc/self/fd/0` through the Electron/process-proxy stack. --- app/src/lib/hooks/hooks-proxy.ts | 35 +++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/src/lib/hooks/hooks-proxy.ts b/app/src/lib/hooks/hooks-proxy.ts index 7f48796d1e5..fb345711bc9 100644 --- a/app/src/lib/hooks/hooks-proxy.ts +++ b/app/src/lib/hooks/hooks-proxy.ts @@ -1,11 +1,13 @@ import { spawn } from 'child_process' -import { basename, resolve } from 'path' +import { basename, resolve, join } from 'path' import { ProcessProxyConnection as Connection } from 'process-proxy' import type { HookCallbackOptions } from '../git' import { resolveGitBinary } from 'dugite' import { ShellEnvResult } from './get-shell-env' import { shellFriendlyNames } from './config' import { Writable } from 'stream' +import { mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' const ignoredOnFailureHooks = [ 'post-applypatch', @@ -64,6 +66,29 @@ const exitWithMessage = (conn: Connection, msg: string, exitCode = 0) => const exitWithError = (conn: Connection, msg: string, exitCode = 1) => exitWithMessage(conn, msg, exitCode) +const readStdin = (stream: NodeJS.ReadableStream) => + new Promise((resolve, reject) => { + const chunks: Buffer[] = [] + + stream.on('data', chunk => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + }) + stream.on('end', () => resolve(Buffer.concat(chunks))) + stream.on('error', reject) + }) + +const createStdinFile = (content: Buffer) => { + const dir = mkdtempSync(join(tmpdir(), 'github-desktop-hooks-')) + const filePath = join(dir, 'stdin.txt') + + writeFileSync(filePath, content) + + return { + filePath, + cleanup: () => rmSync(dir, { recursive: true, force: true }), + } +} + export const createHooksProxy = ( getShellEnv: (cwd: string) => Promise, onHookProgress?: HookCallbackOptions['onHookProgress'], @@ -102,6 +127,8 @@ export const createHooksProxy = ( return } + const stdinFile = hasStdin ? createStdinFile(await readStdin(conn.stdin)) : null + const args = [ ...['hook', 'run', hookName], // We always copy our pre-auto-gc hook in order to be able to tell the @@ -110,7 +137,7 @@ export const createHooksProxy = ( // pre-auto-gc hook configured themselves, so we tell Git to ignore // missing hooks here. ...(hookName === 'pre-auto-gc' ? ['--ignore-missing'] : []), - ...(hasStdin ? ['--to-stdin=/dev/stdin'] : []), + ...(stdinFile ? [`--to-stdin=${stdinFile.filePath}`] : []), '--', ...proxyArgs.slice(1), ] @@ -157,9 +184,11 @@ export const createHooksProxy = ( // https://github.com/git/git/blob/4cf919bd7b946477798af5414a371b23fd68bf93/hook.c#L73C6-L73C22 child.stderr.pipe(conn.stderr, { end: false }).on('error', reject) child.stderr.on('data', data => terminalOutput.push(data)) - conn.stdin.pipe(child.stdin).on('error', reject) + child.stdin.end() }) + stdinFile?.cleanup() + const dur = `after ${((Date.now() - startTime) / 1000).toFixed(2)}s` const prefix = `${hookName} hook` const terminationMessage = signal From 22ac81cfc8a1d9bc77415fa2c17560fb000aaecd Mon Sep 17 00:00:00 2001 From: Pol Rivero <65060696+pol-rivero@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:34:35 +0100 Subject: [PATCH 2/2] Run prettier --- app/src/lib/hooks/hooks-proxy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/lib/hooks/hooks-proxy.ts b/app/src/lib/hooks/hooks-proxy.ts index fb345711bc9..3309a770d6e 100644 --- a/app/src/lib/hooks/hooks-proxy.ts +++ b/app/src/lib/hooks/hooks-proxy.ts @@ -127,7 +127,9 @@ export const createHooksProxy = ( return } - const stdinFile = hasStdin ? createStdinFile(await readStdin(conn.stdin)) : null + const stdinFile = hasStdin + ? createStdinFile(await readStdin(conn.stdin)) + : null const args = [ ...['hook', 'run', hookName],