Home / Alt manpages / gh-codespace-cp(1)

  • gh-codespace-cp(1)
  • User command
  • linux

Copy Files In and Out of a GitHub Codespace with gh codespace cp

You will finish with a repeatable way to copy a file or directory between your local machine and a GitHub Codespace. The examples use GitHub CLI 2.87.3, installed here as gh 2.87.3. Allow about ten minutes if the Codespace already exists and you know its name.

You need an authenticated GitHub CLI installation, a running or available Codespace, and write access to the local destination when downloading. The command may create a public/private SSH key pair in your local ~/.ssh directory on its first connection. That is a local authentication side effect, so check the path and your SSH policy before the first transfer.

1. Check the installed command

Confirm the binary and read the version. These are ordinary, read-only commands and do not need elevated privileges:

$ command -v gh
/usr/bin/gh
$ gh --version
gh version 2.87.3 (2026-02-23)

Then inspect the command contract on the machine where you will run the copy:

$ gh codespace cp --help
The `cp` command copies files between the local and remote file systems.

USAGE
  gh codespace cp [-e] [-r] [-- [<scp flags>...]] <sources>... <dest>

The first argument is a source and the last is the destination. Additional sources are allowed when the destination is a directory. A directory source requires --recursive or -r.

2. Pin the Codespace and check your local paths

Use --codespace when you know the Codespace name. This avoids an interactive selection becoming a distraction in a script or a hurried terminal session:

$ CODESPACE='my-codespace-name'
$ LOCAL_FILE='./README.md'
$ test -f "$LOCAL_FILE" && echo "local source is readable"
local source is readable

Replace the placeholder with the exact name shown by gh codespace list. You can filter instead with --repo OWNER/REPOSITORY or --repo-owner OWNER, but keep the selection explicit when more than one Codespace could match:

$ gh codespace list --repo OWNER/REPOSITORY
NAME                 DISPLAY NAME       STATE
my-codespace-name    project workspace  Available

The displayed columns and state will vary. The useful checkpoint is that the name you pass to --codespace identifies the intended remote machine.

3. Copy one local file to the Codespace

Put the remote: prefix on the remote argument. A remote path without a leading slash is resolved relative to the remote user's home directory:

$ gh codespace cp --codespace "$CODESPACE" \
    "$LOCAL_FILE" 'remote:~/incoming/README.md'
$ test "$?" -eq 0 && echo "upload command completed"
upload command completed

Quote the remote argument. The quotes keep your local shell from interpreting characters intended for the remote path. The destination directory must already be usable by the remote user, and the destination name should be one you are prepared to replace if a file with that name exists.

For a remote path in the workspace, use its absolute path. Codespaces commonly put repositories below /workspaces, but use the path that exists in your own machine:

$ gh codespace cp --codespace "$CODESPACE" \
    ./settings.json 'remote:/workspaces/my-project/settings.json'

4. Copy a remote file back locally

Reverse the source and destination. The remote source is still quoted so that its remote: marker reaches gh intact:

$ mkdir -p ./downloads
$ gh codespace cp --codespace "$CODESPACE" \
    'remote:~/logs/build.log' ./downloads/build.log
$ test -s ./downloads/build.log && echo "download exists and is non-empty"
download exists and is non-empty

mkdir -p changes only your local directory and is normally unprivileged. Do not add sudo merely because the source is remote. If the local destination needs elevated access, create a user-owned staging directory first, inspect the result, then use your normal administrative process to install it.

To copy several remote files, make the final local argument a directory:

$ gh codespace cp --codespace "$CODESPACE" \
    'remote:~/project/go.mod' 'remote:~/project/go.sum' ./downloads/
$ ls -l ./downloads/go.mod ./downloads/go.sum

5. Copy a directory recursively

The recursive flag is mandatory for a directory source. This example downloads a workspace subdirectory into a new local location:

$ gh codespace cp --codespace "$CODESPACE" --recursive \
    'remote:/workspaces/my-project/config' ./config-copy
$ test -d ./config-copy && find ./config-copy -maxdepth 2 -type f -print
./config-copy/app.yml

Keep the destination separate from the source while testing. A recursive copy can create or replace many files, and the command has no transaction to undo a partially completed transfer.

Before a destructive replacement: if the destination contains useful work, stop and make a backup or choose a fresh directory. There is no general undo command for files copied over an existing destination. If you need to abandon the test copy, remove only the explicitly named staging directory after checking it:

$ rm -rf -- ./config-copy
$ test ! -e ./config-copy && echo "staging directory removed"

Do not run that removal against a path you have not inspected. It is irreversible unless you have a separate backup.

6. Expand remote globs only when you mean to

Without --expand, remote file names are interpreted literally. With -e or --expand, each remote argument is evaluated on the remote machine as an scp-style Bash expression. That enables globs, braces, tildes, environment variables and backticks:

$ mkdir -p ./gofiles
$ gh codespace cp --codespace "$CODESPACE" --expand \
    'remote:~/*.go' ./gofiles/
$ find ./gofiles -maxdepth 1 -type f -name '*.go' -print

The outer single quotes are deliberate. They prevent your local shell from expanding the pattern before gh sends it to the Codespace. The remote shell then chooses the matching files.

Security boundary

Do not use --expand with remote path arguments supplied by an untrusted user. Backticks and other shell syntax can cause remote command evaluation. If the names are known, omit --expand and pass literal paths instead.

Brace expansion is useful for a fixed set of known files:

$ gh codespace cp --codespace "$CODESPACE" --expand \
    'remote:/workspaces/my-project/go.{mod,sum}' ./gofiles/

7. Pass an SSH option after the separator

Place -- before additional scp flags. This keeps SSH options separate from gh codespace cp options:

$ gh codespace cp --codespace "$CODESPACE" --expand -- \
    -F "$HOME/.ssh/codespaces_config" \
    'remote:~/*.go' ./gofiles/

Only use an SSH configuration file you have inspected and trust. If you do not need an scp option, leave out the separator and let GitHub CLI manage the connection. The --profile NAME option is a separate GitHub CLI setting for selecting an SSH profile; it is not the same thing as an arbitrary scp flag.

8. Diagnose a failed transfer

Check the simple causes first without changing the Codespace:

$ gh codespace list --repo OWNER/REPOSITORY
$ test -d ./downloads && echo "local destination is writable to this shell"
$ gh codespace cp --help
  • A missing or mistyped Codespace name usually means you should re-run gh codespace list and pass --codespace explicitly.
  • A directory source without --recursive is a usage error. Add -r after confirming the source really is a directory.
  • A wildcard copied literally means you omitted --expand. Add it only for a trusted, reviewed expression.
  • A failure during the first connection may involve the SSH key pair created under ~/.ssh. Check your local SSH permissions and policy before retrying; do not delete keys blindly.

For a repeatable script, test the exit status and write to a staging directory. A zero exit status tells you the command completed successfully, while a separate test, ls or checksum check confirms that the expected local result is present.

Done means

  • The intended Codespace was selected explicitly or verified from the list.
  • Local and remote arguments were placed in the correct source-to-destination order.
  • --recursive was used for every directory source.
  • Remote globs were quoted and expanded only when the expression was trusted.
  • The destination was checked after copying, and useful existing files were backed up or left untouched.
  • You know whether the first connection created SSH keys in ~/.ssh.