Copy Files Safely with scp over SSH

scp moves a file between your machine and an SSH server in one command, and it will happily overwrite the destination if you let it. You will transfer a file, verify the result, and know which options change the security or overwrite behaviour. Allow about ten minutes if your SSH login already works. You need the openssh-client package, a remote account, the server name, and a destination directory you are allowed to write.

These examples use the scp installed with Ubuntu's openssh-client package, version 1:9.6p1-3ubuntu13.19. Since OpenSSH 9.0, this client uses the SFTP protocol by default, which matters if you are reading older instructions that describe scp's historical remote-shell protocol.

1. Check the client and your SSH login

Confirm the command is installed first. Do not use sudo for a normal transfer: reading your own files and writing to a directory you own should be unprivileged.

$ command -v scp
/usr/bin/scp
$ dpkg-query -W -f='${Package} ${Version}\n' openssh-client
openssh-client 1:9.6p1-3ubuntu13.19
$ ssh USER@HOST 'printf "SSH login works\n"'
SSH login works

Replace USER and HOST with the remote account and host you were given. The last command may ask for a password or key passphrase, and it also checks host-key verification, so read a new host-key warning rather than accepting it automatically. A failed SSH login must be fixed before you troubleshoot scp.

Checkpoint: you have a working USER@HOST login and a remote directory path. Continue from here if you are returning to the guide.

2. Upload one file

The basic shape is scp SOURCE USER@HOST:DESTINATION. The colon separates a remote host from its path; quote paths that contain spaces, wildcard characters or shell punctuation.

$ scp ./report.txt USER@HOST:/home/USER/incoming/report.txt
report.txt                                      100%   18KB   1.2MB/s   00:00
$ ssh USER@HOST 'test -s /home/USER/incoming/report.txt && printf "remote file is non-empty\n"'
remote file is non-empty

The progress meter is approximate and varies with the connection. The useful result is the exit status and the remote test: scp exits with status 0 on success and a value greater than 0 on error.

$ printf 'scp exit status: %s\n' "$?"
scp exit status: 0

Warning: if the remote destination already exists, scp can overwrite it, and scp has no undo command. Do not run an overwrite against a valuable file until you have checked the destination and made a backup. To preserve the old remote file first, use an ordinary SSH command if you have permission:

$ ssh USER@HOST 'cp --preserve=all /home/USER/incoming/report.txt /home/USER/incoming/report.txt.bak'
$ scp ./report.txt USER@HOST:/home/USER/incoming/report.txt

After checking the replacement, remove the backup only as a deliberate, separate action. If the upload fails, leave the backup in place and investigate the error.

3. Download a file

Reverse the source and target to copy from the server into the current directory:

$ scp USER@HOST:/home/USER/incoming/report.txt ./report-from-server.txt
report.txt                                      100%   18KB   1.1MB/s   00:00
$ test -s ./report-from-server.txt && sha256sum ./report-from-server.txt
8f...  ./report-from-server.txt

The hash shown is only an example shape, not a value to expect. For a meaningful integrity check, get a trusted hash from the person or system that produced the file and compare it with your local result: a non-empty file and exit status 0 do not prove the contents are trustworthy.

4. Copy a directory and preserve metadata

Use -r for a directory tree. Add -p when the source's modification times, access times and mode bits need preserving:

$ scp -rp ./project USER@HOST:/home/USER/backups/project-2026-09-26
$ ssh USER@HOST 'test -d /home/USER/backups/project-2026-09-26 && find /home/USER/backups/project-2026-09-26 -maxdepth 1 -type f -printf "%f\n" | sort'
README.txt
config.example

Warning: review the source directory before using -r. This client follows symbolic links encountered during traversal, so a link can pull in files outside the apparent tree. If that is not intended, stop and choose a transfer method and source layout that do not expose those links.

Neither -p nor -r needs root when you can read the source and write the destination. Use elevated privileges only when the local file permissions genuinely require them, and consider whether changing ownership or permissions on the destination is actually authorised.

5. Use a non-standard port, key or jump host

Use a capital -P for the SSH port; lowercase -p is already reserved for preserving file metadata:

$ scp -P 2222 ./report.txt USER@HOST:/home/USER/incoming/report.txt
$ scp -i /path/to/private_key ./report.txt USER@HOST:/home/USER/incoming/report.txt
$ scp -J BASTION_USER@BASTION USER@HOST:/home/USER/incoming/report.txt ./report-from-server.txt

Use only a private key file you trust and can protect. Do not paste a private key into a command, put it in a shared directory, or loosen its permissions just to make a transfer work. -J connects through a jump host; it does not make the jump host the final destination.

If you need an SSH setting with no dedicated scp flag, pass it with -o, using the syntax from ssh_config(5). A short connection timeout, for example, helps a script fail rather than wait indefinitely:

$ scp -o ConnectTimeout=10 ./report.txt USER@HOST:/home/USER/incoming/report.txt

6. Handle wildcards and compatibility traps

7. Diagnose a failed transfer

Start with the exact command, paths and permissions. Add -v for SSH and scp diagnostics, but remember that verbose output can expose hostnames, usernames and configuration details in logs:

$ scp -v ./report.txt USER@HOST:/home/USER/incoming/report.txt
$ ssh USER@HOST 'id; test -w /home/USER/incoming && printf "destination is writable\n"'
uid=1001(USER) gid=1001(USER) groups=1001(USER)
destination is writable

A permission error on the remote directory is not fixed by adding local sudo. Ask the remote administrator for an authorised destination, or arrange remote ownership and permissions through the normal change process. A timeout points to the host, port, firewall or jump-host path; an authentication error points to the account, key, agent or server policy.

For automation, add -B so scp does not wait for a password or passphrase prompt, and expect a non-zero status you handle in the calling script. Do not use -q while diagnosing a failure: it suppresses the progress meter and SSH warnings.

Done means