gh cs cp remote: does not resolve remote paths unless `-e` flag is provided
Describe the bug
If I run this:
$ gh cs cp -c ominous-space-journey-wv6jg4qrqh5xqq remote:/workspaces/github/Gemfile Gemfile
/usr/bin/scp: '/workspaces/github/Gemfile': No such file or directory
shell closed: exit status 1
I know the file exists, though. In order to copy it, instead I need to run this:
$ gh cs cp -e -c ominous-space-journey-wv6jg4qrqh5xqq remote:/workspaces/github/Gemfile Gemfile
/usr/bin/scp: '/workspaces/github/Gemfile': No such file or directory
shell closed: exit status 1
The help for cs cp describes the -e flag as "-e, --expand Expand remote file names on remote shell" which in my mind, at least, means expanding ~ and environment variables. However, I am providing absolute paths with no variables.
Affected version
$ gh version
gh version 2.76.2 (2025-07-30)
https://github.com/cli/cli/releases/tag/v2.76.2
+1, though running into this on gh version 2.74.2 (2025-06-17). I think I ran into this issue back in July this year, but didn't think much of it until another team member started having issues with this too today (they're running 2.45).
I looked through the code in this repo earlier today, and the issue appears to be incorrect escaping of the arguments passed to scp.
In https://github.com/cli/cli/blob/trunk/pkg/cmd/codespace/ssh.go#L771, when the -e option isn't provided, the user input for a remote: prefixed source/dest gets wrapped in single quotes (ie. remote:/path/to/file -> remote:'/path/to/file). The scp command itself is constructed at https://github.com/cli/cli/blob/6b19a854710ff2c81070f109f35434cd20e40115/internal/codespaces/ssh.go#L137 using exec.CommandContext, which handles escaping arguments. This seems to transform what is intended to be scp remote:'/path/to/file' local into scp "remote:'/path/to/file'" local, which causes errors, as the remote host interprets the single-quotes as part of the file path, rather than an escaped path.
I'm hoping to find some time in the next few days to work on a PR for this issue.
Hey @babakks 👋 I looked through the code in this repo earlier today, and the issue appears to be due to incorrect escaping of arguments passed to scp.
In pkg/cmd/codespace/cp.go,
when the -e option isn’t provided, the user input for a remote:-prefixed source/destination gets wrapped in single quotes,
for example:
if !opts.expand {
arg = `remote:'` + strings.Replace(rest, `'`, `'\''`, -1) + `'`
}
This turns something like:
remote:/workspaces/github/Gemfile
into:
remote:'/workspaces/github/Gemfile'
Later, the command itself is constructed in cli/internal/codespaces/ssh.go:
cmd := exec.CommandContext(ctx, exe, cmdArgs...)
Since exec.CommandContext already handles argument escaping, wrapping the remote path in additional quotes results in double-escaping — effectively transforming what should be:
scp remote:/workspaces/github/Gemfile local
into:
scp "remote:'/workspaces/github/Gemfile'" local
The remote host then interprets the single quotes as literal characters in the path, leading to:
/usr/bin/scp: '/workspaces/github/Gemfile': No such file or directory
When -e is used, this quoting logic is skipped, which explains why it works correctly in that case.