Unable to use scp or sftp when using PowerShell as the subsystem unless the profile is basically blank · Issue #2430 · PowerShell/Win32-OpenSSH · GitHub
Skip to content

Unable to use scp or sftp when using PowerShell as the subsystem unless the profile is basically blank #2430

Open

Description

@robertstrom

Prerequisites

  • Write a descriptive title.
  • Make sure you are able to repro it on the latest version
  • Search the existing issues.

Steps to reproduce

I am working on configuring an enterprise to have Windows OpenSSH installed on all systems that support the feature.

At that time I am also ensuring that PowerShell v7 is installed and that the SSH subsystem is pwsh.

I have that all working. What I have an issue with is if there is a PowerShell profile of any significance neither scp or sftp will work.

I have spent significant time "talking" with AI about this and it says (and I have basically proved it out) that the PowerShell profile needs to be "clean"

"When sftp or scp connects, it starts a session to initialize the subsystem. If your PowerShell profile contains commands that produce visual output (like a custom prompt, a "Welcome" banner, or a tool update check), that text is sent over the data stream. The SFTP client expects a specific binary protocol header; when it receives "Welcome to PowerShell" instead, it chokes and terminates the connection."

Subsystem       powershell      C:\PROGRA~1\POWERS~1\7\pwsh.exe -SSHS -NoProfile -NoLogo
Subsystem	sftp	C:\Windows\System32\OpenSSH\sftp-server.exe

So I either have to:

  • Use CMD.EXE as the shell ... that IS NOT going to happen
  • Use a totally neutered, or ZERO PowerShell profile - That is not going to happen either. I want all users to have a prompt that has the time, the domain, the user, the computer name, and the directory that they are in
  • Or, jump through hoops to get scp and sftp to work and have a real profile

Expected behavior

scp and sftp work when the shell is PowerShell and you have an actual profile

Preference #1 - fix it and make it just work ... always ... like *nix
Preference #2 - document that it doesn't work, why it doesn't work, and example(s) on how to fix it

Actual behavior

connection closed

Error details

Environment data

This is from the most recent machine that I was doing this on but I have happened on Server 2019 and Server 2022 also


Name                           Value
----                           -----
PSVersion                      7.5.4
PSEdition                      Core
GitCommitId                    7.5.4
OS                             Microsoft Windows 10.0.26200
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Version

OpenSSH_for_Windows_9.5p2, LibreSSL 3.8.2

Visuals

No response

Activity

  1. jborean93 commented on Mar 10, 2026

    @jborean93

    I'm honestly surprised that calling a subsystem would invoke the shell at all as the config has the full executable and args that can be invoked directly but low and behold

    Image

    Would be nice to avoid it altogether so things like this wouldn't happen and we don't have the overhead of starting yet another executable to just invoke a single exe specified by the subsystem.

  2. ZhenyuXiao commented on Mar 17, 2026

    @ZhenyuXiao

    I encountered this problem before, and I solved it by adding the following code to the beginning of my PowerShell profile.
    If ([Environment]::CommandLine -like '*sftp-server.exe*') { Start-Process 'sftp-server.exe' -NoNewWindow -Wait [Environment]::Exit(0) }
    Command referenced from #1291 (comment)
    It was just a workaround.

  3. robertstrom commented on Mar 17, 2026

    @robertstrom
    Author

    ZhenYu Xiao (@ZhenyuXiao) / All,

    That link is from 2018 .... yikes. I will tuck your solution away. Don't know that one is better than the other. I posted my solution below.

    I solved it too, but it was totally unexpected behavior, and it took a fair amount of time to come up with a solution.

    Changing the default shell to PowerShell is well documented, and I believe that it would be the preferred shell. This issue / behavior should, IMHO, absolutely be documented and explained. This is definitely not the way that things behave in the *Nix world when it comes to SSH and SFTP.

    SSH on Windows is definitely it's own beast with significant differences in some behaviors from *Nix SSH ;-)

    My solution:

    # This needs to be placed at the very top of any PowerShell profile
    # The content enclosed within this "try / catch" statement is to ensure that scp and sftp still work
    # Without this content both scp and sftp fail
    try {
        $isSshSession = -not [string]::IsNullOrEmpty($env:SSH_CONNECTION)
        $hasTty       = -not [string]::IsNullOrEmpty($env:SSH_TTY)
    
        # Non-interactive SSH command (scp/sftp) → MUST be silent
        if ($isSshSession -and -not $hasTty) {
            return
        }
    }
    catch {
        return
    }
    

    This is what I am working on as a standardized PowerShell Profile. My understanding is that this would be a base profile and that individuals own profiles would override any settings that conflict. The goal is to ensure that everyone knows the system that they are logged into. The custom prompt has the time, the AD domain, the AD user name, the computer name, and the current directory, each in a different color.

    One thing that I ran across in some testing is the way that an SSH banner behaves in Windows when configured in thesshd_config file. It just flashes by and it isn't really presented to the user and you cannot scroll back to see it. Setting up the banner this way solves that problem.

    I have mentioned the banner issue to Tess Gauthier (@tgauth) and others in MSFT. Don't know if that is something that is being worked on or not.

    Custom PowerShell profile. This displays for SSH and for interactive logon. There was one situation that I found where it did not change the profile though.

    Tess Gauthier (@tgauth) - why would this not work when I do PowerShell Remoting over SSH. This profile gets setup when doing a normal SSH, but not when doing Enter-PSSession -Hostname <user_name>@<computer_name> when using SSH keys

    # ===========================
    # Install Corporate Banner + Prompt for PS5 + PS7 (AllUsersCurrentHost)
    # Run as Administrator
    # ===========================
    
    # Build the combined profile content (banner + prompt)
    $ProfileContent = @'
    # ---- DO NOT OUTPUT ANYTHING ABOVE THIS LINE ----
    # The content enclosed within this "try / catch" statement is to ensure that scp and sftp still work
    # Without this content both scp and sftp fail
    try {
        $isSshSession = -not [string]::IsNullOrEmpty($env:SSH_CONNECTION)
        $hasTty       = -not [string]::IsNullOrEmpty($env:SSH_TTY)
    
        # Non-interactive SSH command (scp/sftp) → MUST be silent
        if ($isSshSession -and -not $hasTty) {
            return
        }
    }
    catch {
        return
    }
    
    # ==================================================
    # Corporate Banner (prints once per session)
    # ==================================================
    if (-not $global:CorpBannerShown) {
        $global:CorpBannerShown = $true
    
        # Optional: keep banner visible even if screen was cluttered
        Write-Host ""
    
        # Header
        Write-Host "**********************" -ForegroundColor Yellow
        Write-Host "    W A R N I N G"     -ForegroundColor Yellow
        Write-Host "**********************" -ForegroundColor Yellow
        Write-Host ""
        Write-Host "          THIS IS A PRIVATE COMPUTER SYSTEM" -ForegroundColor Yellow
        Write-Host ""
    
        # Body (exact text you provided)
        Write-Host "This computer system including all related equipment, network devices" -ForegroundColor Gray
        Write-Host "(specifically including Internet Access), are provided only for authorized use." -ForegroundColor Gray
        Write-Host "All computer systems may be monitored for all lawful purposes, including to" -ForegroundColor Gray
        Write-Host "ensure that their use is authorized, for management of the system, to" -ForegroundColor Gray
        Write-Host "facilitate protection against unauthorized access, and to verify security" -ForegroundColor Gray
        Write-Host "procedures, survivability and operational security. Monitoring includes active" -ForegroundColor Gray
        Write-Host "attacks by authorized personnel and their entities to test or verify the" -ForegroundColor Gray
        Write-Host "security of the system. During monitoring, information may be examined," -ForegroundColor Gray
        Write-Host "recorded, copied and used for authorized purposes. All information including" -ForegroundColor Gray
        Write-Host "personal information, placed on or sent over this system may be monitored." -ForegroundColor Gray
        Write-Host "Uses of this system, authorized or unauthorized, constitutes consent to" -ForegroundColor Gray
        Write-Host "monitoring of this system. Unauthorized use may subject you to criminal" -ForegroundColor Gray
        Write-Host "prosecution. Evidence of any such unauthorized use collected during" -ForegroundColor Gray
        Write-Host "monitoring may be used for administrative, criminal or other adverse action." -ForegroundColor Gray
        Write-Host "Use of this system constitutes consent to monitoring for these purposes." -ForegroundColor Gray
        Write-Host ""
    
        # Footer / Help line
        Write-Host "LOGIN ISSUES?  PLEASE CONTACT THE <INSERT YOUR COMANY NAME>" -ForegroundColor Cyan
        Write-Host "SERVICE DESK AT (888) 5555-1212"            -ForegroundColor Cyan
        Write-Host ""
    }
    
    # ==================================================
    # Custom Prompt (two lines, multicolor)
    # ==================================================
    Function Prompt {
        $User = $env:USERNAME
        $Domain = $env:USERDOMAIN
        $Computer = $env:COMPUTERNAME
        $Dir = (Get-Location).Path
        $Time = Get-Date -Format "HH:mm:ss"
    
        # Start Bracket
        Write-Host "[" -NoNewline -ForegroundColor White
    
        # 1. Time (Gray)
        Write-Host "$Time" -NoNewline -ForegroundColor Gray
        Write-Host " | " -NoNewline -ForegroundColor White
    
        # 2. Domain (White)
        Write-Host "$Domain" -NoNewline -ForegroundColor White
    
        # The Single Backslash (White)
        Write-Host "\" -NoNewline -ForegroundColor White
    
        # 3. User (Green)
        Write-Host "$User" -NoNewline -ForegroundColor Green
    
        # 4. Machine (Cyan)
        Write-Host "@$Computer" -NoNewline -ForegroundColor Cyan
    
        Write-Host " " -NoNewline
    
        # 5. Directory (Yellow)
        Write-Host "$Dir" -NoNewline -ForegroundColor Yellow
    
        # End Bracket
        Write-Host "]" -ForegroundColor White
    
        # Line 2: The actual prompt
        Write-Host "PS >" -NoNewline -ForegroundColor White
        return " "
    }
    '@
    
    # --- Target profile locations for AllUsersCurrentHost (PS5 + PS7) ---
    $Targets = @()
    
    # Windows PowerShell 5.1 (powershell.exe)
    $ps5AllUsersCurrentHost = Join-Path $env:WINDIR 'System32\WindowsPowerShell\v1.0\Microsoft.PowerShell_profile.ps1'
    $Targets += $ps5AllUsersCurrentHost
    
    # PowerShell 7+ (pwsh.exe) if installed
    $pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
    if ($pwshCmd) {
        $ps7Home = Split-Path -Parent $pwshCmd.Source
        $ps7AllUsersCurrentHost = Join-Path $ps7Home 'Microsoft.PowerShell_profile.ps1'
        $Targets += $ps7AllUsersCurrentHost
    }
    
    # --- Write the content to each target profile ---
    foreach ($Path in $Targets) {
        $dir = Split-Path -Parent $Path
        New-Item -Path $dir -ItemType Directory -Force | Out-Null
    
        # UTF8 works well for both PS5 and PS7 (and supports future non-ASCII text)
        Set-Content -Path $Path -Value $ProfileContent -Encoding UTF8 -Force
    
        Write-Host "Applied banner+prompt to: $Path" -ForegroundColor Magenta
    }
    
    if (-not $pwshCmd) {
        Write-Host "Note: pwsh.exe not found, so only PS5 profile was updated." -ForegroundColor Yellow
    }
    
  4. tgauth commented on Mar 17, 2026

    @tgauth
    Collaborator

    robertstrom, can you open an issue on the PowerShell repo regarding the Enter-PSSession behavior?

  5. robertstrom commented on Mar 17, 2026

    @robertstrom
    Author

    Tess Gauthier (@tgauth) - I can ... but I found this right after I posted.

    https://github.com/MicrosoftDocs/PowerShell-Docs/blob/main/reference/docs-conceptual/security/remoting/SSH-Remoting-in-PowerShell.md#limitations

    "PSRemoting over SSH doesn't support Profiles and doesn't have access to $PROFILE. Once in a session, you can load a profile by dot sourcing the profile with the full filepath. This isn't related to SSH profiles. You can configure the SSH server to use PowerShell as the default shell and to load a profile through SSH. See the SSH documentation for more information."

    There are some work-arounds for personal profiles, but what I am wanting to do is load a common profile (AllUsers).

    do you still want me to open an issue?

  6. tgauth commented on Mar 18, 2026

    @tgauth
    Collaborator

    do you still want me to open an issue?

    No, thanks for linking the docs!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions