mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 08:31:19 +00:00
fix(ssh): use /bin/bash full path to bypass zsh profile sourcing
SSH passes commands to the remote's login shell (zsh on this system) which parses them. Using just 'bash' still causes zsh to source its profile files while resolving the command path, triggering the "zsh:35: parse error" from .zshrc. Using /bin/bash directly bypasses this - zsh executes the absolute path without sourcing any profile files first.
This commit is contained in:
@@ -320,7 +320,7 @@ describe('ssh-command-builder', () => {
|
||||
|
||||
const lastArg = result.args[result.args.length - 1];
|
||||
// Command is wrapped in bash with PATH setup (no profile sourcing)
|
||||
expect(lastArg).toContain('bash --norc --noprofile -c');
|
||||
expect(lastArg).toContain('/bin/bash --norc --noprofile -c');
|
||||
expect(lastArg).toContain('export PATH=');
|
||||
expect(lastArg).toContain('claude');
|
||||
expect(lastArg).toContain('--print');
|
||||
@@ -381,9 +381,9 @@ describe('ssh-command-builder', () => {
|
||||
});
|
||||
|
||||
const wrappedCommand = result.args[result.args.length - 1];
|
||||
// The command is wrapped in bash --norc --noprofile -c "..." with PATH setup
|
||||
// The command is wrapped in /bin/bash --norc --noprofile -c "..." with PATH setup
|
||||
// $ signs are escaped as \$ to prevent expansion by SSH's outer shell
|
||||
expect(wrappedCommand).toContain('bash --norc --noprofile -c');
|
||||
expect(wrappedCommand).toContain('/bin/bash --norc --noprofile -c');
|
||||
expect(wrappedCommand).toContain('git');
|
||||
expect(wrappedCommand).toContain('commit');
|
||||
expect(wrappedCommand).toContain('fix:');
|
||||
@@ -637,7 +637,7 @@ describe('ssh-command-builder', () => {
|
||||
});
|
||||
|
||||
const wrappedCommand = result.args[result.args.length - 1];
|
||||
// The prompt is wrapped in bash --norc --noprofile -c "..." with double-quote escaping
|
||||
// The prompt is wrapped in /bin/bash --norc --noprofile -c "..." with double-quote escaping
|
||||
// $PATH in the PROMPT should be escaped as \$PATH to prevent expansion
|
||||
// (Note: the PATH setup also contains $PATH but that's intentional)
|
||||
expect(wrappedCommand).toContain("'\\\\$PATH variable");
|
||||
|
||||
@@ -273,19 +273,23 @@ export async function buildSshCommand(
|
||||
// Wrap the command with explicit PATH setup instead of sourcing profile files.
|
||||
// Profile files often chain to zsh or contain syntax incompatible with -c embedding.
|
||||
//
|
||||
// CRITICAL: Use /bin/bash (full path) instead of just 'bash' because:
|
||||
// SSH passes the command to the remote's login shell (often zsh) which parses it.
|
||||
// If we use 'bash', zsh still sources its profile files while resolving the command.
|
||||
// Using /bin/bash directly bypasses this - zsh just executes the path without sourcing.
|
||||
//
|
||||
// We prepend common binary locations to PATH:
|
||||
// - ~/.local/bin: Claude Code, pip --user installs
|
||||
// - ~/bin: User scripts
|
||||
// - /usr/local/bin: Homebrew on Intel Mac, manual installs
|
||||
// - /opt/homebrew/bin: Homebrew on Apple Silicon
|
||||
// - ~/.cargo/bin: Rust tools
|
||||
// - ~/.nvm/versions/node/*/bin: Node.js via nvm (glob doesn't work, but current version does)
|
||||
//
|
||||
// This approach avoids all profile sourcing issues while ensuring agent binaries are found.
|
||||
const pathPrefix =
|
||||
'export PATH="$HOME/.local/bin:$HOME/bin:/usr/local/bin:/opt/homebrew/bin:$HOME/.cargo/bin:$PATH"';
|
||||
const escapedCommand = shellEscapeForDoubleQuotes(remoteCommand);
|
||||
const wrappedCommand = `bash --norc --noprofile -c "${pathPrefix} && ${escapedCommand}"`;
|
||||
const wrappedCommand = `/bin/bash --norc --noprofile -c "${pathPrefix} && ${escapedCommand}"`;
|
||||
args.push(wrappedCommand);
|
||||
|
||||
// Log the exact command being built - use info level so it appears in system logs
|
||||
|
||||
Reference in New Issue
Block a user