cli icon indicating copy to clipboard operation
cli copied to clipboard

gh cs cp remote: does not resolve remote paths unless `-e` flag is provided

Open offbyone opened this issue 3 months ago • 3 comments

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

offbyone avatar Sep 11 '25 14:09 offbyone

+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).

Matthew-Kilpatrick avatar Sep 12 '25 13:09 Matthew-Kilpatrick

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.

Matthew-Kilpatrick avatar Sep 18 '25 14:09 Matthew-Kilpatrick

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.

2003Aditya avatar Nov 07 '25 16:11 2003Aditya