From b2d516674c71617d63e27abc5af45acd1306de31 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 09:03:36 -0600 Subject: [PATCH 1/7] =?UTF-8?q?=F0=9F=A4=96=20feat:=20pass=20PROJECT=5FPAT?= =?UTF-8?q?H=20env=20var=20to=20init=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LocalRuntime: Pass PROJECT_PATH when spawning init hook - SSHRuntime: Pass PROJECT_PATH in env when executing init hook remotely This allows init hooks to reference the project path without relying on inferring it from the workspace path. --- src/runtime/LocalRuntime.ts | 4 ++++ src/runtime/SSHRuntime.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/runtime/LocalRuntime.ts b/src/runtime/LocalRuntime.ts index e64c162ab..0e161e2ac 100644 --- a/src/runtime/LocalRuntime.ts +++ b/src/runtime/LocalRuntime.ts @@ -415,6 +415,10 @@ export class LocalRuntime implements Runtime { const proc = spawn("bash", ["-c", `"${hookPath}"`], { cwd: workspacePath, stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + PROJECT_PATH: projectPath, + }, }); proc.stdout.on("data", (data: Buffer) => { diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index c35eb2e35..3363ae431 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -734,6 +734,9 @@ export class SSHRuntime implements Runtime { cwd: workspacePath, // Run in the workspace directory timeout: 3600, // 1 hour - generous timeout for init hooks abortSignal, + env: { + PROJECT_PATH: projectPath, + }, }); // Create line-buffered loggers From ee95d32d315115f2398e8bdb561f7aff2c111f85 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 09:11:57 -0600 Subject: [PATCH 2/7] =?UTF-8?q?=F0=9F=A4=96=20docs:=20document=20PROJECT?= =?UTF-8?q?=5FPATH=20env=20var=20in=20init=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added new 'Environment Variables' section documenting that init hooks receive PROJECT_PATH pointing to the project root directory. Includes example showing how to reference files in the project root from within a workspace-scoped init hook. --- docs/init-hooks.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/init-hooks.md b/docs/init-hooks.md index eedfbd6ee..8b1f4a026 100644 --- a/docs/init-hooks.md +++ b/docs/init-hooks.md @@ -27,6 +27,29 @@ chmod +x .cmux/init The init script runs in the workspace directory with the workspace's environment. +## Environment Variables + +Init hooks receive the following environment variables: + +- `PROJECT_PATH` - Absolute path to the project root directory + +Example usage: + +```bash +#!/bin/bash +set -e + +echo "Project root: $PROJECT_PATH" +echo "Workspace directory: $PWD" + +# Reference files in project root +if [ -f "$PROJECT_PATH/.env" ]; then + cp "$PROJECT_PATH/.env" "$PWD/.env" +fi + +bun install +``` + ## Use Cases - Install dependencies (`npm install`, `bun install`, etc.) From 019bd4c4ef74813c224fea7ba6258bfdb623c85d Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 10:07:14 -0600 Subject: [PATCH 3/7] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20centralize=20ini?= =?UTF-8?q?t=20hook=20env=20var=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract getInitHookEnv() helper in initHook.ts to avoid duplicating environment variable logic between LocalRuntime and SSHRuntime. This makes it easier to add new init hook env vars in the future - just update the helper function instead of maintaining duplicate env objects. --- src/runtime/LocalRuntime.ts | 4 ++-- src/runtime/SSHRuntime.ts | 6 ++---- src/runtime/initHook.ts | 10 ++++++++++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/runtime/LocalRuntime.ts b/src/runtime/LocalRuntime.ts index 0e161e2ac..8d3f20bd9 100644 --- a/src/runtime/LocalRuntime.ts +++ b/src/runtime/LocalRuntime.ts @@ -20,7 +20,7 @@ import { RuntimeError as RuntimeErrorClass } from "./Runtime"; import { NON_INTERACTIVE_ENV_VARS } from "../constants/env"; import { EXIT_CODE_ABORTED, EXIT_CODE_TIMEOUT } from "../constants/exitCodes"; import { listLocalBranches } from "../git"; -import { checkInitHookExists, getInitHookPath, createLineBufferedLoggers } from "./initHook"; +import { checkInitHookExists, getInitHookPath, createLineBufferedLoggers, getInitHookEnv } from "./initHook"; import { execAsync, DisposableProcess } from "../utils/disposableExec"; import { getProjectName } from "../utils/runtime/helpers"; import { getErrorMessage } from "../utils/errors"; @@ -417,7 +417,7 @@ export class LocalRuntime implements Runtime { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, - PROJECT_PATH: projectPath, + ...getInitHookEnv(projectPath), }, }); diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index 3363ae431..eebc917fd 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -18,7 +18,7 @@ import type { import { RuntimeError as RuntimeErrorClass } from "./Runtime"; import { EXIT_CODE_ABORTED, EXIT_CODE_TIMEOUT } from "../constants/exitCodes"; import { log } from "../services/log"; -import { checkInitHookExists, createLineBufferedLoggers } from "./initHook"; +import { checkInitHookExists, createLineBufferedLoggers, getInitHookEnv } from "./initHook"; import { streamProcessToLogger } from "./streamProcess"; import { expandTildeForSSH, cdCommandForSSH } from "./tildeExpansion"; import { getProjectName } from "../utils/runtime/helpers"; @@ -734,9 +734,7 @@ export class SSHRuntime implements Runtime { cwd: workspacePath, // Run in the workspace directory timeout: 3600, // 1 hour - generous timeout for init hooks abortSignal, - env: { - PROJECT_PATH: projectPath, - }, + env: getInitHookEnv(projectPath), }); // Create line-buffered loggers diff --git a/src/runtime/initHook.ts b/src/runtime/initHook.ts index 401b71f00..5228edc0d 100644 --- a/src/runtime/initHook.ts +++ b/src/runtime/initHook.ts @@ -26,6 +26,16 @@ export function getInitHookPath(projectPath: string): string { return path.join(projectPath, ".cmux", "init"); } +/** + * Get environment variables for init hook execution + * Centralizes env var injection to avoid duplication across runtimes + */ +export function getInitHookEnv(projectPath: string): Record { + return { + PROJECT_PATH: projectPath, + }; +} + /** * Line-buffered logger that splits stream output into lines and logs them * Handles incomplete lines by buffering until a newline is received From 2a5d30a8559eb562f1139176d92b3ee044f06585 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 10:17:26 -0600 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=A4=96=20ci:=20fix=20formatting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/runtime/LocalRuntime.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/runtime/LocalRuntime.ts b/src/runtime/LocalRuntime.ts index 8d3f20bd9..b23e2352b 100644 --- a/src/runtime/LocalRuntime.ts +++ b/src/runtime/LocalRuntime.ts @@ -20,7 +20,12 @@ import { RuntimeError as RuntimeErrorClass } from "./Runtime"; import { NON_INTERACTIVE_ENV_VARS } from "../constants/env"; import { EXIT_CODE_ABORTED, EXIT_CODE_TIMEOUT } from "../constants/exitCodes"; import { listLocalBranches } from "../git"; -import { checkInitHookExists, getInitHookPath, createLineBufferedLoggers, getInitHookEnv } from "./initHook"; +import { + checkInitHookExists, + getInitHookPath, + createLineBufferedLoggers, + getInitHookEnv, +} from "./initHook"; import { execAsync, DisposableProcess } from "../utils/disposableExec"; import { getProjectName } from "../utils/runtime/helpers"; import { getErrorMessage } from "../utils/errors"; From 2952a0d38a2152cd559afc3e1a5cdf1db377f069 Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 10:18:18 -0600 Subject: [PATCH 5/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20use=20remote=20projec?= =?UTF-8?q?t=20path=20for=20SSH=20init=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SSH init hooks now receive PROJECT_PATH pointing to the remote project root (srcBaseDir/projectName) instead of the local project path. This ensures init scripts can access project-level files on the remote filesystem as documented in docs/init-hooks.md. --- src/runtime/SSHRuntime.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index eebc917fd..44f08f9d3 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -728,13 +728,18 @@ export class SSHRuntime implements Runtime { // Tilde won't be expanded when the path is quoted, so we need to expand it ourselves const hookCommand = expandTildeForSSH(remoteHookPath); + // Compute remote project path for PROJECT_PATH env var + // workspacePath is srcBaseDir/projectName/branchName, so parent is the remote project root + const projectName = getProjectName(projectPath); + const remoteProjectPath = path.posix.join(this.config.srcBaseDir, projectName); + // Run hook remotely and stream output // No timeout - user init hooks can be arbitrarily long const hookStream = await this.exec(hookCommand, { cwd: workspacePath, // Run in the workspace directory timeout: 3600, // 1 hour - generous timeout for init hooks abortSignal, - env: getInitHookEnv(projectPath), + env: getInitHookEnv(remoteProjectPath), }); // Create line-buffered loggers From 568c26a59cf889cb4801b57fa65278b167476fbb Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 10:30:19 -0600 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20add=20MUX=5F=20p?= =?UTF-8?q?refix=20and=20MUX=5FRUNTIME=20env=20var?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed init hook environment variables: - PROJECT_PATH → MUX_PROJECT_PATH - Added MUX_RUNTIME ("local" | "ssh") MUX_RUNTIME is important because: - On local workspaces, MUX_PROJECT_PATH points to local filesystem - On SSH workspaces, MUX_PROJECT_PATH points to remote filesystem - Scripts can check MUX_RUNTIME to handle platform differences Updated docs with examples showing runtime-specific behavior. --- docs/init-hooks.md | 19 +++++++++++++++---- src/runtime/LocalRuntime.ts | 2 +- src/runtime/SSHRuntime.ts | 2 +- src/runtime/initHook.ts | 10 ++++++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/docs/init-hooks.md b/docs/init-hooks.md index 8b1f4a026..38dbb4bbe 100644 --- a/docs/init-hooks.md +++ b/docs/init-hooks.md @@ -31,7 +31,10 @@ The init script runs in the workspace directory with the workspace's environment Init hooks receive the following environment variables: -- `PROJECT_PATH` - Absolute path to the project root directory +- `MUX_PROJECT_PATH` - Absolute path to the project root directory + - **Local workspaces**: Path on your local machine + - **SSH workspaces**: Path on the remote machine +- `MUX_RUNTIME` - Runtime type: `"local"` or `"ssh"` Example usage: @@ -39,12 +42,20 @@ Example usage: #!/bin/bash set -e -echo "Project root: $PROJECT_PATH" +echo "Runtime: $MUX_RUNTIME" +echo "Project root: $MUX_PROJECT_PATH" echo "Workspace directory: $PWD" # Reference files in project root -if [ -f "$PROJECT_PATH/.env" ]; then - cp "$PROJECT_PATH/.env" "$PWD/.env" +if [ -f "$MUX_PROJECT_PATH/.env" ]; then + cp "$MUX_PROJECT_PATH/.env" "$PWD/.env" +fi + +# Runtime-specific behavior +if [ "$MUX_RUNTIME" = "local" ]; then + echo "Running on local machine" +else + echo "Running on SSH remote" fi bun install diff --git a/src/runtime/LocalRuntime.ts b/src/runtime/LocalRuntime.ts index b23e2352b..f6750720e 100644 --- a/src/runtime/LocalRuntime.ts +++ b/src/runtime/LocalRuntime.ts @@ -422,7 +422,7 @@ export class LocalRuntime implements Runtime { stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, - ...getInitHookEnv(projectPath), + ...getInitHookEnv(projectPath, "local"), }, }); diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index 44f08f9d3..fe863f800 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -739,7 +739,7 @@ export class SSHRuntime implements Runtime { cwd: workspacePath, // Run in the workspace directory timeout: 3600, // 1 hour - generous timeout for init hooks abortSignal, - env: getInitHookEnv(remoteProjectPath), + env: getInitHookEnv(remoteProjectPath, "ssh"), }); // Create line-buffered loggers diff --git a/src/runtime/initHook.ts b/src/runtime/initHook.ts index 5228edc0d..70862f3cf 100644 --- a/src/runtime/initHook.ts +++ b/src/runtime/initHook.ts @@ -29,10 +29,16 @@ export function getInitHookPath(projectPath: string): string { /** * Get environment variables for init hook execution * Centralizes env var injection to avoid duplication across runtimes + * @param projectPath - Path to project root (local path for LocalRuntime, remote path for SSHRuntime) + * @param runtime - Runtime type: "local" or "ssh" */ -export function getInitHookEnv(projectPath: string): Record { +export function getInitHookEnv( + projectPath: string, + runtime: "local" | "ssh" +): Record { return { - PROJECT_PATH: projectPath, + MUX_PROJECT_PATH: projectPath, + MUX_RUNTIME: runtime, }; } From 95ff9bc569c248261501a8802a6d272f5db5732c Mon Sep 17 00:00:00 2001 From: Ammar Date: Thu, 13 Nov 2025 10:31:45 -0600 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=A4=96=20fix:=20use=20local=20project?= =?UTF-8?q?=20path=20for=20MUX=5FPROJECT=5FPATH=20on=20SSH?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MUX_PROJECT_PATH now always refers to the local project path, even on SSH workspaces. This is more useful for logging and debugging. Updated docs to clarify: - MUX_PROJECT_PATH = local project path (both local and SSH) - Use relative paths (e.g., '../.env') in init hooks to reference project files, which works for both local and SSH workspaces --- docs/init-hooks.md | 18 ++++++++++-------- src/runtime/SSHRuntime.ts | 7 +------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/docs/init-hooks.md b/docs/init-hooks.md index 38dbb4bbe..3207e6cfe 100644 --- a/docs/init-hooks.md +++ b/docs/init-hooks.md @@ -31,24 +31,26 @@ The init script runs in the workspace directory with the workspace's environment Init hooks receive the following environment variables: -- `MUX_PROJECT_PATH` - Absolute path to the project root directory - - **Local workspaces**: Path on your local machine - - **SSH workspaces**: Path on the remote machine +- `MUX_PROJECT_PATH` - Absolute path to the project root on the **local machine** + - Always refers to your local project path, even on SSH workspaces + - Useful for logging, debugging, or runtime-specific logic - `MUX_RUNTIME` - Runtime type: `"local"` or `"ssh"` + - Use this to detect whether the hook is running locally or remotely -Example usage: +**Note for SSH workspaces:** Since the project is synced to the remote machine, files exist in both locations. The init hook runs in the workspace directory (`$PWD`), so use relative paths to reference project files: ```bash #!/bin/bash set -e echo "Runtime: $MUX_RUNTIME" -echo "Project root: $MUX_PROJECT_PATH" +echo "Local project path: $MUX_PROJECT_PATH" echo "Workspace directory: $PWD" -# Reference files in project root -if [ -f "$MUX_PROJECT_PATH/.env" ]; then - cp "$MUX_PROJECT_PATH/.env" "$PWD/.env" +# Copy .env from project root (works for both local and SSH) +# The hook runs with cwd = workspace, and project root is the parent directory +if [ -f "../.env" ]; then + cp "../.env" "$PWD/.env" fi # Runtime-specific behavior diff --git a/src/runtime/SSHRuntime.ts b/src/runtime/SSHRuntime.ts index fe863f800..902f1cf30 100644 --- a/src/runtime/SSHRuntime.ts +++ b/src/runtime/SSHRuntime.ts @@ -728,18 +728,13 @@ export class SSHRuntime implements Runtime { // Tilde won't be expanded when the path is quoted, so we need to expand it ourselves const hookCommand = expandTildeForSSH(remoteHookPath); - // Compute remote project path for PROJECT_PATH env var - // workspacePath is srcBaseDir/projectName/branchName, so parent is the remote project root - const projectName = getProjectName(projectPath); - const remoteProjectPath = path.posix.join(this.config.srcBaseDir, projectName); - // Run hook remotely and stream output // No timeout - user init hooks can be arbitrarily long const hookStream = await this.exec(hookCommand, { cwd: workspacePath, // Run in the workspace directory timeout: 3600, // 1 hour - generous timeout for init hooks abortSignal, - env: getInitHookEnv(remoteProjectPath, "ssh"), + env: getInitHookEnv(projectPath, "ssh"), }); // Create line-buffered loggers