Unable to use scp or sftp when using PowerShell as the subsystem unless the profile is basically blank · Issue #2430 · PowerShell/Win32-OpenSSH · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Unable to use scp or sftp when using PowerShell as the subsystem unless the profile is basically blank #2430
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."
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 profilePreference #1 - fix it and make it just work ... always ... like *nixPreference #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
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
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.
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.
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
}
"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).
Activity
jborean93 commentedon Mar 10, 2026
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
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.
ZhenyuXiao commentedon Mar 17, 2026
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.
robertstrom commentedon Mar 17, 2026
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 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 the
sshd_configfile. 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 keystgauth commentedon Mar 17, 2026
robertstrom, can you open an issue on the PowerShell repo regarding the
Enter-PSSessionbehavior?robertstrom commentedon Mar 17, 2026
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?
tgauth commentedon Mar 18, 2026
No, thanks for linking the docs!