fix: SSH compatibility with fish shell on remote host

- Remove bash -c from remote_exec (pass command as single SSH arg)
- Fix remote_write_file to pass bash -c script as single quoted string
- Add LogLevel=ERROR to suppress SSH warning messages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-07 23:17:45 +09:00
parent 98e55ab1a5
commit ae691e557c

View File

@@ -22,6 +22,7 @@ def _ssh_base_cmd() -> list[str]:
"ssh", "ssh",
"-o", "StrictHostKeyChecking=no", "-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null", "-o", "UserKnownHostsFile=/dev/null",
"-o", "LogLevel=ERROR",
"-o", "BatchMode=yes", "-o", "BatchMode=yes",
"-o", "ConnectTimeout=10", "-o", "ConnectTimeout=10",
"-p", str(SSH_PORT), "-p", str(SSH_PORT),
@@ -35,6 +36,9 @@ def _ssh_base_cmd() -> list[str]:
def remote_exec(command: str, timeout: int = SUBPROCESS_TIMEOUT) -> subprocess.CompletedProcess: def remote_exec(command: str, timeout: int = SUBPROCESS_TIMEOUT) -> subprocess.CompletedProcess:
"""Execute a command on the remote host via SSH. """Execute a command on the remote host via SSH.
The command is passed as a single SSH argument so the remote shell
interprets it as-is (compatible with fish, bash, zsh).
Args: Args:
command: Shell command to execute remotely command: Shell command to execute remotely
timeout: Command timeout in seconds timeout: Command timeout in seconds
@@ -46,7 +50,7 @@ def remote_exec(command: str, timeout: int = SUBPROCESS_TIMEOUT) -> subprocess.C
subprocess.TimeoutExpired: If command times out subprocess.TimeoutExpired: If command times out
OSError: If SSH command fails to execute OSError: If SSH command fails to execute
""" """
ssh_cmd = _ssh_base_cmd() + ["bash", "-c", command] ssh_cmd = _ssh_base_cmd() + [command]
logger.debug("SSH exec: %s", command) logger.debug("SSH exec: %s", command)
return subprocess.run( return subprocess.run(
ssh_cmd, ssh_cmd,
@@ -81,7 +85,8 @@ def remote_read_file(path: str) -> str:
def remote_write_file(path: str, content: str) -> None: def remote_write_file(path: str, content: str) -> None:
"""Write content to a file on the remote host atomically. """Write content to a file on the remote host atomically.
Uses temp file + mv for atomic write, matching local behavior. Uses bash explicitly for bash-specific syntax ($(mktemp), $tmpf).
The entire script is passed as a single argument to 'bash -c'.
Args: Args:
path: Absolute file path on remote host path: Absolute file path on remote host
@@ -91,9 +96,9 @@ def remote_write_file(path: str, content: str) -> None:
IOError: If write fails IOError: If write fails
""" """
ssh_cmd = _ssh_base_cmd() ssh_cmd = _ssh_base_cmd()
# Atomic write: write to temp file, then rename (force bash for compatibility) # Atomic write via bash (single-quoted to survive remote shell interpretation)
remote_script = f"tmpf=$(mktemp {path}.tmp.XXXXXX) && cat > \"$tmpf\" && mv \"$tmpf\" {path}" remote_script = f"tmpf=$(mktemp {path}.tmp.XXXXXX) && cat >\"$tmpf\" && mv \"$tmpf\" {path}"
ssh_cmd.extend(["bash", "-c", remote_script]) ssh_cmd.append(f"bash -c '{remote_script}'")
logger.debug("SSH write: %s (%d bytes)", path, len(content)) logger.debug("SSH write: %s (%d bytes)", path, len(content))
result = subprocess.run( result = subprocess.run(